From 55f9509c6e680529198a390c41994cdf5540bb4d Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 2 Sep 2024 02:25:04 -0400 Subject: [PATCH 01/18] Add a `MinecraftVarintLengthCompositeEncoder` for backend connections (#1419) This is an optimization that has been in BungeeCord for some time, but it's an idea that makes a lot of sense: avoid a memory copy when we don't need to do one. The OS will hopefully be smart enough to use `writev()` or similar for this. --- .../network/BackendChannelInitializer.java | 4 +- ...MinecraftVarintLengthCompositeEncoder.java | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java index 1e6f387b3..f6fb43ea6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java @@ -30,7 +30,7 @@ import com.velocitypowered.proxy.protocol.netty.AutoReadHolderHandler; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthCompositeEncoder; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.handler.timeout.ReadTimeoutHandler; @@ -55,7 +55,7 @@ public class BackendChannelInitializer extends ChannelInitializer { .addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.MILLISECONDS)) - .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(FRAME_ENCODER, MinecraftVarintLengthCompositeEncoder.INSTANCE) .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) .addLast(FLOW_HANDLER, new AutoReadHolderHandler()) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java new file mode 100644 index 000000000..18679b161 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.netty; + +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import java.util.List; + +/** + * Handler for appending a length for Minecraft packets using composite buffers. + */ +@ChannelHandler.Sharable +public class MinecraftVarintLengthCompositeEncoder extends MessageToMessageEncoder { + + public static final MinecraftVarintLengthCompositeEncoder INSTANCE = new MinecraftVarintLengthCompositeEncoder(); + + private MinecraftVarintLengthCompositeEncoder() { + } + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf buf, + List list) throws Exception { + ByteBuf varIntBuffer = ctx.alloc().ioBuffer(ProtocolUtils.varIntBytes(buf.readableBytes())); + ProtocolUtils.writeVarInt(varIntBuffer, buf.readableBytes()); + list.add(varIntBuffer); + list.add(buf.retain()); + } +} From a7654af1a3c5885c96769a05d43a5604dc47de10 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 2 Sep 2024 02:57:03 -0400 Subject: [PATCH 02/18] Do some minor natives housekeeping. --- .../natives/compression/CompressorUtils.java | 14 --------- .../natives/util/MoreByteBufUtils.java | 31 ++++++------------- .../natives/util/NativeConstraints.java | 2 -- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java b/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java index 8230ba204..94cde4bd5 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java @@ -26,20 +26,6 @@ class CompressorUtils { */ static final int ZLIB_BUFFER_SIZE = 8192; - /** - * Ensures that the buffer does not go over {@code max}. - * - * @param buf the buffer for check - * @param max the maximum size for the buffer - * @throws DataFormatException if the buffer becomes too bug - */ - static void ensureMaxSize(ByteBuf buf, int max) throws DataFormatException { - int len = buf.readableBytes(); - if (len > max) { - throw new DataFormatException("Got too much data (" + len + " > " + max + ")"); - } - } - private CompressorUtils() { throw new AssertionError(); } diff --git a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java index fbad62bf9..13f4f0c57 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java +++ b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java @@ -52,18 +52,13 @@ public class MoreByteBufUtils { private static boolean isCompatible(Native nativeStuff, ByteBuf buf) { BufferPreference preferred = nativeStuff.preferredBufferType(); - switch (preferred) { - case DIRECT_PREFERRED: - case HEAP_PREFERRED: + return switch (preferred) { + case DIRECT_PREFERRED, HEAP_PREFERRED -> // The native prefers this type, but doesn't strictly require we provide it. - return true; - case DIRECT_REQUIRED: - return buf.hasMemoryAddress(); - case HEAP_REQUIRED: - return buf.hasArray(); - default: - throw new AssertionError("Preferred buffer type unknown"); - } + true; + case DIRECT_REQUIRED -> buf.hasMemoryAddress(); + case HEAP_REQUIRED -> buf.hasArray(); + }; } /** @@ -77,15 +72,9 @@ public class MoreByteBufUtils { */ public static ByteBuf preferredBuffer(ByteBufAllocator alloc, Native nativeStuff, int initialCapacity) { - switch (nativeStuff.preferredBufferType()) { - case HEAP_REQUIRED: - case HEAP_PREFERRED: - return alloc.heapBuffer(initialCapacity); - case DIRECT_PREFERRED: - case DIRECT_REQUIRED: - return alloc.directBuffer(initialCapacity); - default: - throw new AssertionError("Preferred buffer type unknown"); - } + return switch (nativeStuff.preferredBufferType()) { + case HEAP_REQUIRED, HEAP_PREFERRED -> alloc.heapBuffer(initialCapacity); + case DIRECT_PREFERRED, DIRECT_REQUIRED -> alloc.directBuffer(initialCapacity); + }; } } diff --git a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java index f707ab248..04e5faff0 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java +++ b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java @@ -39,8 +39,6 @@ public class NativeConstraints { } String osArch = System.getProperty("os.arch", ""); - // HotSpot on Intel macOS prefers x86_64, but OpenJ9 on macOS and HotSpot/OpenJ9 elsewhere - // give amd64. IS_AMD64 = osArch.equals("amd64") || osArch.equals("x86_64"); IS_AARCH64 = osArch.equals("aarch64") || osArch.equals("arm64"); } From 862036d42466cdd1f6bf00db0e3efe12d97d9e0c Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 2 Sep 2024 03:00:19 -0400 Subject: [PATCH 03/18] Fix a compile error. --- .../velocitypowered/natives/compression/CompressorUtils.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java b/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java index 94cde4bd5..3cfe0e671 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/CompressorUtils.java @@ -17,9 +17,6 @@ package com.velocitypowered.natives.compression; -import io.netty.buffer.ByteBuf; -import java.util.zip.DataFormatException; - class CompressorUtils { /** * The default preferred output buffer size for zlib. From 52ae735ea32911ab4e3e145999a6408072bf663e Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 2 Sep 2024 20:47:01 -0400 Subject: [PATCH 04/18] Change `LoginInboundConnection` queue type to something that is actually thread-safe --- .../proxy/connection/client/LoginInboundConnection.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java index 93064a790..837376f95 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java @@ -30,9 +30,9 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import java.net.InetSocketAddress; -import java.util.ArrayDeque; import java.util.Optional; import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -58,7 +58,7 @@ public class LoginInboundConnection implements LoginPhaseConnection, KeyIdentifi InitialInboundConnection delegate) { this.delegate = delegate; this.outstandingResponses = Int2ObjectSyncMap.hashmap(); - this.loginMessagesToSend = new ArrayDeque<>(); + this.loginMessagesToSend = new ConcurrentLinkedQueue<>(); } @Override From f034c0277d60e9f0a80e484832aec831cbc298f7 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Tue, 3 Sep 2024 00:33:57 -0400 Subject: [PATCH 05/18] Have one `MinecraftVarintLengthEncoder`, not two Applies more to the margins (such as login phase and server list ping), but every bit does help. --- .../network/BackendChannelInitializer.java | 6 ++-- .../netty/MinecraftVarintLengthEncoder.java | 31 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java index f6fb43ea6..d60c64775 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/BackendChannelInitializer.java @@ -30,7 +30,7 @@ import com.velocitypowered.proxy.protocol.netty.AutoReadHolderHandler; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthCompositeEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.handler.timeout.ReadTimeoutHandler; @@ -49,13 +49,13 @@ public class BackendChannelInitializer extends ChannelInitializer { } @Override - protected void initChannel(Channel ch) throws Exception { + protected void initChannel(Channel ch) { ch.pipeline() .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) .addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.MILLISECONDS)) - .addLast(FRAME_ENCODER, MinecraftVarintLengthCompositeEncoder.INSTANCE) + .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) .addLast(FLOW_HANDLER, new AutoReadHolderHandler()) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java index ecbcc8b84..115d8b472 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java @@ -23,33 +23,34 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.handler.codec.MessageToMessageEncoder; +import java.util.List; /** * Handler for appending a length for Minecraft packets. */ @ChannelHandler.Sharable -public class MinecraftVarintLengthEncoder extends MessageToByteEncoder { +public class MinecraftVarintLengthEncoder extends MessageToMessageEncoder { public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder(); - public static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY; + + static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY; private MinecraftVarintLengthEncoder() { } @Override - protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { - ProtocolUtils.writeVarInt(out, msg.readableBytes()); - out.writeBytes(msg); - } + protected void encode(ChannelHandlerContext ctx, ByteBuf buf, + List list) throws Exception { + final int length = buf.readableBytes(); + final int varintLength = ProtocolUtils.varIntBytes(length); - @Override - protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) - throws Exception { - int anticipatedRequiredCapacity = ProtocolUtils.varIntBytes(msg.readableBytes()) - + msg.readableBytes(); - return IS_JAVA_CIPHER - ? ctx.alloc().heapBuffer(anticipatedRequiredCapacity) - : ctx.alloc().directBuffer(anticipatedRequiredCapacity); + final ByteBuf lenBuf = IS_JAVA_CIPHER + ? ctx.alloc().heapBuffer(varintLength) + : ctx.alloc().directBuffer(varintLength); + + ProtocolUtils.writeVarInt(lenBuf, length); + list.add(lenBuf); + list.add(buf.retain()); } } From 46f29480bd62a556976a728246ccdd6fab03c774 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Tue, 3 Sep 2024 00:40:31 -0400 Subject: [PATCH 06/18] Delete now-unused `MinecraftVarintLengthCompositeEncoder` --- ...MinecraftVarintLengthCompositeEncoder.java | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java deleted file mode 100644 index 18679b161..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthCompositeEncoder.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2018-2023 Velocity Contributors - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.netty; - -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageEncoder; -import java.util.List; - -/** - * Handler for appending a length for Minecraft packets using composite buffers. - */ -@ChannelHandler.Sharable -public class MinecraftVarintLengthCompositeEncoder extends MessageToMessageEncoder { - - public static final MinecraftVarintLengthCompositeEncoder INSTANCE = new MinecraftVarintLengthCompositeEncoder(); - - private MinecraftVarintLengthCompositeEncoder() { - } - - @Override - protected void encode(ChannelHandlerContext ctx, ByteBuf buf, - List list) throws Exception { - ByteBuf varIntBuffer = ctx.alloc().ioBuffer(ProtocolUtils.varIntBytes(buf.readableBytes())); - ProtocolUtils.writeVarInt(varIntBuffer, buf.readableBytes()); - list.add(varIntBuffer); - list.add(buf.retain()); - } -} From 784806848d486fd3c06c3f63a8c54483361b9241 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Thu, 5 Sep 2024 00:00:34 -0400 Subject: [PATCH 07/18] Do more speculative VarInt reading optimizations (#1418) --- .../proxy/protocol/ProtocolUtils.java | 75 ++++------ .../netty/MinecraftVarintFrameDecoder.java | 130 +++++++++++++----- .../protocol/netty/VarintByteDecoder.java | 68 --------- .../packet/LoginPluginMessagePacket.java | 2 +- .../proxy/protocol/ProtocolUtilsTest.java | 4 +- 5 files changed, 127 insertions(+), 152 deletions(-) delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 053dcd65f..797a58223 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -104,6 +104,7 @@ public enum ProtocolUtils { .build(); public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB + private static final int MAXIMUM_VARINT_SIZE = 5; private static final BinaryTagType[] BINARY_TAG_TYPES = new BinaryTagType[] { BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT, BinaryTagTypes.LONG, BinaryTagTypes.FLOAT, BinaryTagTypes.DOUBLE, @@ -111,13 +112,18 @@ public enum ProtocolUtils { BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY}; private static final QuietDecoderException BAD_VARINT_CACHED = new QuietDecoderException("Bad VarInt decoded"); - private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33]; + private static final int[] VAR_INT_LENGTHS = new int[65]; static { for (int i = 0; i <= 32; ++i) { - VARINT_EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d); + VAR_INT_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d); } - VARINT_EXACT_BYTE_LENGTHS[32] = 1; // Special case for the number 0. + VAR_INT_LENGTHS[32] = 1; // Special case for the number 0. + } + + private static DecoderException badVarint() { + return MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded") + : BAD_VARINT_CACHED; } /** @@ -127,56 +133,29 @@ public enum ProtocolUtils { * @return the decoded VarInt */ public static int readVarInt(ByteBuf buf) { - int read = readVarIntSafely(buf); - if (read == Integer.MIN_VALUE) { - throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded") - : BAD_VARINT_CACHED; + int readable = buf.readableBytes(); + if (readable == 0) { + // special case for empty buffer + throw badVarint(); } - return read; - } - /** - * Reads a Minecraft-style VarInt from the specified {@code buf}. The difference between this - * method and {@link #readVarInt(ByteBuf)} is that this function returns a sentinel value if the - * varint is invalid. - * - * @param buf the buffer to read from - * @return the decoded VarInt, or {@code Integer.MIN_VALUE} if the varint is invalid - */ - public static int readVarIntSafely(ByteBuf buf) { - int i = 0; - int maxRead = Math.min(5, buf.readableBytes()); - for (int j = 0; j < maxRead; j++) { - int k = buf.readByte(); + // we can read at least one byte, and this should be a common case + int k = buf.readByte(); + if ((k & 0x80) != 128) { + return k; + } + + // in case decoding one byte was not enough, use a loop to decode up to the next 4 bytes + int maxRead = Math.min(MAXIMUM_VARINT_SIZE, readable); + int i = k & 0x7F; + for (int j = 1; j < maxRead; j++) { + k = buf.readByte(); i |= (k & 0x7F) << j * 7; if ((k & 0x80) != 128) { return i; } } - return Integer.MIN_VALUE; - } - - /** - * Reads a Minecraft-style VarInt from the specified {@code buf}. The difference between this - * method and {@link #readVarInt(ByteBuf)} is that this function returns a sentinel value if the - * varint is invalid. - * - * @param buf the buffer to read from - * @return the decoded VarInt - * @throws DecoderException if the varint is invalid - */ - public static int readVarIntSafelyOrThrow(ByteBuf buf) { - int i = 0; - int maxRead = Math.min(5, buf.readableBytes()); - for (int j = 0; j < maxRead; j++) { - int k = buf.readByte(); - i |= (k & 0x7F) << j * 7; - if ((k & 0x80) != 128) { - return i; - } - } - throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded") - : BAD_VARINT_CACHED; + throw badVarint(); } /** @@ -186,7 +165,7 @@ public enum ProtocolUtils { * @return the byte size of {@code value} if encoded as a VarInt */ public static int varIntBytes(int value) { - return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)]; + return VAR_INT_LENGTHS[Integer.numberOfLeadingZeros(value)]; } /** @@ -210,6 +189,8 @@ public enum ProtocolUtils { private static void writeVarIntFull(ByteBuf buf, int value) { // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ + + // This essentially is an unrolled version of the "traditional" VarInt encoding. if ((value & (0xFFFFFFFF << 7)) == 0) { buf.writeByte(value); } else if ((value & (0xFFFFFFFF << 14)) == 0) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java index 94baa2ffe..7bf7563ea 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java @@ -17,7 +17,8 @@ package com.velocitypowered.proxy.protocol.netty; -import com.velocitypowered.proxy.protocol.netty.VarintByteDecoder.DecodeResult; +import static io.netty.util.ByteProcessor.FIND_NON_NUL; + import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -29,53 +30,114 @@ import java.util.List; */ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { - private static final QuietDecoderException BAD_LENGTH_CACHED = + private static final QuietDecoderException BAD_PACKET_LENGTH = new QuietDecoderException("Bad packet length"); - private static final QuietDecoderException VARINT_BIG_CACHED = + private static final QuietDecoderException VARINT_TOO_BIG = new QuietDecoderException("VarInt too big"); @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) + throws Exception { if (!ctx.channel().isActive()) { in.clear(); return; } - final VarintByteDecoder reader = new VarintByteDecoder(); - - int varintEnd = in.forEachByte(reader); - if (varintEnd == -1) { - // We tried to go beyond the end of the buffer. This is probably a good sign that the - // buffer was too short to hold a proper varint. - if (reader.getResult() == DecodeResult.RUN_OF_ZEROES) { - // Special case where the entire packet is just a run of zeroes. We ignore them all. - in.clear(); - } + // skip any runs of 0x00 we might find + int packetStart = in.forEachByte(FIND_NON_NUL); + if (packetStart == -1) { return; } + in.readerIndex(packetStart); - if (reader.getResult() == DecodeResult.RUN_OF_ZEROES) { - // this will return to the point where the next varint starts - in.readerIndex(varintEnd); - } else if (reader.getResult() == DecodeResult.SUCCESS) { - int readVarint = reader.getReadVarint(); - int bytesRead = reader.getBytesRead(); - if (readVarint < 0) { - in.clear(); - throw BAD_LENGTH_CACHED; - } else if (readVarint == 0) { - // skip over the empty packet(s) and ignore it - in.readerIndex(varintEnd + 1); + // try to read the length of the packet + in.markReaderIndex(); + int preIndex = in.readerIndex(); + int length = readRawVarInt21(in); + if (preIndex == in.readerIndex()) { + return; + } + if (length < 0) { + throw BAD_PACKET_LENGTH; + } + + // note that zero-length packets are ignored + if (length > 0) { + if (in.readableBytes() < length) { + in.resetReaderIndex(); } else { - int minimumRead = bytesRead + readVarint; - if (in.isReadable(minimumRead)) { - out.add(in.retainedSlice(varintEnd + 1, readVarint)); - in.skipBytes(minimumRead); - } + out.add(in.readRetainedSlice(length)); } - } else if (reader.getResult() == DecodeResult.TOO_BIG) { - in.clear(); - throw VARINT_BIG_CACHED; } } + + /** + * Reads a VarInt from the buffer of up to 21 bits in size. + * + * @param buffer the buffer to read from + * @return the VarInt decoded, {@code 0} if no varint could be read + * @throws QuietDecoderException if the VarInt is too big to be decoded + */ + private static int readRawVarInt21(ByteBuf buffer) { + if (buffer.readableBytes() < 4) { + // we don't have enough that we can read a potentially full varint, so fall back to + // the slow path. + return readRawVarintSmallBuf(buffer); + } + int wholeOrMore = buffer.getIntLE(buffer.readerIndex()); + + // take the last three bytes and check if any of them have the high bit set + int atStop = ~wholeOrMore & 0x808080; + if (atStop == 0) { + // all bytes have the high bit set, so the varint we are trying to decode is too wide + throw VARINT_TOO_BIG; + } + + int bitsToKeep = Integer.numberOfTrailingZeros(atStop) + 1; + buffer.skipBytes(bitsToKeep >> 3); + + // remove all bits we don't need to keep, a trick from + // https://github.com/netty/netty/pull/14050#issuecomment-2107750734: + // + // > The idea is that thisVarintMask has 0s above the first one of firstOneOnStop, and 1s at + // > and below it. For example if firstOneOnStop is 0x800080 (where the last 0x80 is the only + // > one that matters), then thisVarintMask is 0xFF. + // + // this is also documented in Hacker's Delight, section 2-1 "Manipulating Rightmost Bits" + int preservedBytes = wholeOrMore & (atStop ^ (atStop - 1)); + + // merge together using this trick: https://github.com/netty/netty/pull/14050#discussion_r1597896639 + preservedBytes = (preservedBytes & 0x007F007F) | ((preservedBytes & 0x00007F00) >> 1); + preservedBytes = (preservedBytes & 0x00003FFF) | ((preservedBytes & 0x3FFF0000) >> 2); + return preservedBytes; + } + + private static int readRawVarintSmallBuf(ByteBuf buffer) { + if (!buffer.isReadable()) { + return 0; + } + buffer.markReaderIndex(); + + byte tmp = buffer.readByte(); + if (tmp >= 0) { + return tmp; + } + int result = tmp & 0x7F; + if (!buffer.isReadable()) { + buffer.resetReaderIndex(); + return 0; + } + if ((tmp = buffer.readByte()) >= 0) { + return result | tmp << 7; + } + result |= (tmp & 0x7F) << 7; + if (!buffer.isReadable()) { + buffer.resetReaderIndex(); + return 0; + } + if ((tmp = buffer.readByte()) >= 0) { + return result | tmp << 14; + } + return result | (tmp & 0x7F) << 14; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java deleted file mode 100644 index 06cec7350..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2020-2021 Velocity Contributors - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.netty; - -import io.netty.util.ByteProcessor; - -class VarintByteDecoder implements ByteProcessor { - - private int readVarint; - private int bytesRead; - private DecodeResult result = DecodeResult.TOO_SHORT; - - @Override - public boolean process(byte k) { - if (k == 0 && bytesRead == 0) { - // tentatively say it's invalid, but there's a possibility of redemption - result = DecodeResult.RUN_OF_ZEROES; - return true; - } - if (result == DecodeResult.RUN_OF_ZEROES) { - return false; - } - readVarint |= (k & 0x7F) << bytesRead++ * 7; - if (bytesRead > 3) { - result = DecodeResult.TOO_BIG; - return false; - } - if ((k & 0x80) != 128) { - result = DecodeResult.SUCCESS; - return false; - } - return true; - } - - public int getReadVarint() { - return readVarint; - } - - public int getBytesRead() { - return bytesRead; - } - - public DecodeResult getResult() { - return result; - } - - public enum DecodeResult { - SUCCESS, - TOO_SHORT, - TOO_BIG, - RUN_OF_ZEROES - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessagePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessagePacket.java index 213492cbf..682785eb2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessagePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessagePacket.java @@ -63,7 +63,7 @@ public class LoginPluginMessagePacket extends DeferredByteBufHolder implements M @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - this.id = ProtocolUtils.readVarIntSafelyOrThrow(buf); + this.id = ProtocolUtils.readVarInt(buf); this.channel = ProtocolUtils.readString(buf); if (buf.isReadable()) { this.replace(buf.readRetainedSlice(buf.readableBytes())); diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java index f476de67c..1ed59cd83 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java @@ -70,7 +70,7 @@ public class ProtocolUtilsTest { private void writeReadTestOld(ByteBuf buf, int test) { buf.clear(); writeVarIntOld(buf, test); - assertEquals(test, ProtocolUtils.readVarIntSafely(buf)); + assertEquals(test, ProtocolUtils.readVarInt(buf)); } @Test @@ -103,7 +103,7 @@ public class ProtocolUtilsTest { "Encoding of " + i + " was invalid"); assertEquals(i, oldReadVarIntSafely(varintNew)); - assertEquals(i, ProtocolUtils.readVarIntSafely(varintOld)); + assertEquals(i, ProtocolUtils.readVarInt(varintOld)); varintNew.clear(); varintOld.clear(); From 525ac2712e8e090ed7f1366fe0bc499f3b6f5b74 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sat, 7 Sep 2024 12:56:31 -0400 Subject: [PATCH 08/18] Also provide musl libc builds of Velocity natives Reference: PaperMC/Paper#11367 --- native/README.md | 6 +-- native/build-support/alpine.Dockerfile | 9 +++++ .../build-support/build-all-linux-natives.sh | 11 ++--- .../build-support/compile-linux-compress.sh | 8 +++- native/build-support/compile-linux-crypto.sh | 8 ++-- .../natives/util/NativeConstraints.java | 38 ++++++++++++++++-- .../velocitypowered/natives/util/Natives.java | 20 ++++++++- .../velocity-cipher-ossl30x-musl.so | Bin 0 -> 71528 bytes .../linux_aarch64/velocity-compress-musl.so | Bin 0 -> 75752 bytes .../velocity-cipher-ossl30x-musl.so | Bin 0 -> 17232 bytes .../linux_x86_64/velocity-compress-musl.so | Bin 0 -> 103696 bytes 11 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 native/build-support/alpine.Dockerfile create mode 100755 native/src/main/resources/linux_aarch64/velocity-cipher-ossl30x-musl.so create mode 100755 native/src/main/resources/linux_aarch64/velocity-compress-musl.so create mode 100755 native/src/main/resources/linux_x86_64/velocity-cipher-ossl30x-musl.so create mode 100755 native/src/main/resources/linux_x86_64/velocity-compress-musl.so diff --git a/native/README.md b/native/README.md index 327edfdc7..81e4e3b6c 100644 --- a/native/README.md +++ b/native/README.md @@ -18,13 +18,11 @@ CommonCrypto library. - macOS aarch64 ("Apple Silicon") For Linux platforms, we provide two versions of the native library: one built against OpenSSL 1.1.x and one built against OpenSSL 3.x.x. -All native libraries are built on various versions of Ubuntu: +All native libraries are built on various versions of Ubuntu and Alpine: - Ubuntu 20.04 for OpenSSL 1.1.x support and for compression - Ubuntu 22.04 for OpenSSL 3.x.x support - -Currently, we do not provide native libraries for distributions based on musl libc, like Alpine Linux. You might be able to use `apk add libc6-compat` to fake it, but this is not officially supported. -In the future we may provide a musl libc build. +- Alpine 3.18 for OpenSSL 3.x.x support and compression (musl libc users only) ## Building diff --git a/native/build-support/alpine.Dockerfile b/native/build-support/alpine.Dockerfile new file mode 100644 index 000000000..c67cd822d --- /dev/null +++ b/native/build-support/alpine.Dockerfile @@ -0,0 +1,9 @@ + +FROM amazoncorretto:17.0.12-alpine3.18 + +# Install required dependencies +RUN apk add --no-cache bash alpine-sdk cmake openssl-dev openssl + +# Create a non-root user +RUN adduser -D user +USER user \ No newline at end of file diff --git a/native/build-support/build-all-linux-natives.sh b/native/build-support/build-all-linux-natives.sh index 32397f591..0e182da69 100755 --- a/native/build-support/build-all-linux-natives.sh +++ b/native/build-support/build-all-linux-natives.sh @@ -6,7 +6,8 @@ set -e cd "$(dirname "$0")/.." || exit 1 ARCHS=(x86_64 aarch64) -BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy) +BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine) +COMPRESSION_VARIANTS=(ubuntu-focal alpine) for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do docker_platforms="" @@ -25,8 +26,8 @@ for arch in "${ARCHS[@]}"; do docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh" done - # Use only the oldest variant for the compression library - variant=${BASE_DOCKERFILE_VARIANTS[0]} - echo "Building native compression for $arch on $variant..." - docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh" + for variant in "${COMPRESSION_VARIANTS[@]}"; do + echo "Building native compression for $arch on $variant..." + docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh" + done done \ No newline at end of file diff --git a/native/build-support/compile-linux-compress.sh b/native/build-support/compile-linux-compress.sh index b03ca37cf..2222c4a31 100755 --- a/native/build-support/compile-linux-compress.sh +++ b/native/build-support/compile-linux-compress.sh @@ -16,8 +16,14 @@ cd libdeflate || exit rm -rf build && cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -B build && cmake --build build --target libdeflate_static cd .. +# Determine if we are on musl libc or glibc +suffix="" +if ldd --version 2>&1 | grep -q musl; then + suffix="-musl" +fi + CFLAGS="-O2 -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -Wl,-z,noexecstack -Wall -Werror -fomit-frame-pointer" ARCH=$(uname -m) mkdir -p src/main/resources/linux_$ARCH $CC $CFLAGS -Ilibdeflate src/main/c/jni_util.c src/main/c/jni_zlib_deflate.c src/main/c/jni_zlib_inflate.c \ - libdeflate/build/libdeflate.a -o src/main/resources/linux_$ARCH/velocity-compress.so \ No newline at end of file + libdeflate/build/libdeflate.a -o src/main/resources/linux_$ARCH/velocity-compress$suffix.so \ No newline at end of file diff --git a/native/build-support/compile-linux-crypto.sh b/native/build-support/compile-linux-crypto.sh index 221656647..d29a96918 100755 --- a/native/build-support/compile-linux-crypto.sh +++ b/native/build-support/compile-linux-crypto.sh @@ -20,9 +20,11 @@ if [ ! "$CC" ]; then export CC=gcc fi -output_file="velocity-cipher.so" -if [ -n "$OPENSSL_VERSION" ]; then - output_file="velocity-cipher-ossl${OPENSSL_VERSION}.so" +# Determine if we are on musl libc or glibc +suffix="" +if ldd --version 2>&1 | grep -q musl; then + suffix="-musl" + filename=$(echo "$filename" | sed "s/\.so/$suffix.so/") fi CFLAGS="-O2 -I$JAVA_HOME/include/ -I$JAVA_HOME/include/linux/ -fPIC -shared -Wl,-z,noexecstack -Wall -Werror -fomit-frame-pointer" diff --git a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java index 04e5faff0..852a1f062 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java +++ b/native/src/main/java/com/velocitypowered/natives/util/NativeConstraints.java @@ -19,6 +19,7 @@ package com.velocitypowered.natives.util; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; import java.util.function.BooleanSupplier; /** @@ -29,6 +30,8 @@ public class NativeConstraints { private static final boolean IS_AMD64; private static final boolean IS_AARCH64; private static final boolean CAN_GET_MEMORYADDRESS; + private static final boolean IS_LINUX; + private static final boolean IS_MUSL_LIBC; static { ByteBuf test = Unpooled.directBuffer(); @@ -41,17 +44,44 @@ public class NativeConstraints { String osArch = System.getProperty("os.arch", ""); IS_AMD64 = osArch.equals("amd64") || osArch.equals("x86_64"); IS_AARCH64 = osArch.equals("aarch64") || osArch.equals("arm64"); + + IS_LINUX = System.getProperty("os.name", "").equalsIgnoreCase("Linux"); + + // Determine if we're using musl libc by invoking `ldd --version`. + if (IS_LINUX) { + boolean isMusl; + try { + Process process = new ProcessBuilder("ldd", "--version") + .redirectErrorStream(true) + .start(); + process.waitFor(); + try (var reader = process.getInputStream()) { + byte[] outputRaw = reader.readAllBytes(); + String output = new String(outputRaw, StandardCharsets.UTF_8); + isMusl = output.contains("musl"); + } + } catch (Exception e) { + isMusl = false; + } + IS_MUSL_LIBC = isMusl; + } else { + IS_MUSL_LIBC = false; + } } static final BooleanSupplier NATIVE_BASE = () -> NATIVES_ENABLED && CAN_GET_MEMORYADDRESS; static final BooleanSupplier LINUX_X86_64 = () -> NATIVE_BASE.getAsBoolean() - && System.getProperty("os.name", "").equalsIgnoreCase("Linux") - && IS_AMD64; + && IS_LINUX && IS_AMD64 && !IS_MUSL_LIBC; + + static final BooleanSupplier LINUX_X86_64_MUSL = () -> NATIVE_BASE.getAsBoolean() + && IS_LINUX && IS_AMD64 && IS_MUSL_LIBC; static final BooleanSupplier LINUX_AARCH64 = () -> NATIVE_BASE.getAsBoolean() - && System.getProperty("os.name", "").equalsIgnoreCase("Linux") - && IS_AARCH64; + && IS_LINUX && IS_AARCH64 && !IS_MUSL_LIBC; + + static final BooleanSupplier LINUX_AARCH64_MUSL = () -> NATIVE_BASE.getAsBoolean() + && IS_LINUX && IS_AARCH64 && IS_MUSL_LIBC; static final BooleanSupplier MACOS_AARCH64 = () -> NATIVE_BASE.getAsBoolean() && System.getProperty("os.name", "").equalsIgnoreCase("Mac OS X") diff --git a/native/src/main/java/com/velocitypowered/natives/util/Natives.java b/native/src/main/java/com/velocitypowered/natives/util/Natives.java index 1c1e26cea..8c99fe51a 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/Natives.java +++ b/native/src/main/java/com/velocitypowered/natives/util/Natives.java @@ -83,11 +83,21 @@ public class Natives { new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_X86_64, copyAndLoadNative("/linux_x86_64/velocity-compress.so"), "libdeflate (Linux x86_64)", - LibdeflateVelocityCompressor.FACTORY), // compiled with Debian 10 + LibdeflateVelocityCompressor.FACTORY), // compiled with Ubuntu 20.04 + new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_X86_64_MUSL, + copyAndLoadNative("/linux_x86_64/velocity-compress-musl.so"), + "libdeflate (Linux x86_64, musl)", + LibdeflateVelocityCompressor.FACTORY), // compiled with Alpine 3.18 + new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_AARCH64, copyAndLoadNative("/linux_aarch64/velocity-compress.so"), "libdeflate (Linux aarch64)", - LibdeflateVelocityCompressor.FACTORY), // compiled with Fedora 36 + LibdeflateVelocityCompressor.FACTORY), // compiled with Ubuntu 20.04 + new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_AARCH64_MUSL, + copyAndLoadNative("/linux_aarch64/velocity-compress-musl.so"), + "libdeflate (Linux aarch64, musl)", + LibdeflateVelocityCompressor.FACTORY), // compiled with Alpine 3.18 + new NativeCodeLoader.Variant<>(NativeConstraints.MACOS_AARCH64, copyAndLoadNative("/macos_arm64/velocity-compress.dylib"), "libdeflate (macOS ARM64 / Apple Silicon)", @@ -108,6 +118,9 @@ public class Natives { new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_X86_64, copyAndLoadNative("/linux_x86_64/velocity-cipher-ossl11x.so"), // Ubuntu 20.04 "OpenSSL 1.1.x (Linux x86_64)", NativeVelocityCipher.FACTORY), + new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_X86_64_MUSL, + copyAndLoadNative("/linux_x86_64/velocity-cipher-ossl30x-musl.so"), // Alpine 3.18 + "OpenSSL 3.x.x (Linux x86_64, musl)", NativeVelocityCipher.FACTORY), new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_AARCH64, copyAndLoadNative("/linux_aarch64/velocity-cipher.so"), @@ -118,6 +131,9 @@ public class Natives { new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_AARCH64, copyAndLoadNative("/linux_aarch64/velocity-cipher-ossl11x.so"), "OpenSSL 1.1.x (Linux aarch64)", NativeVelocityCipher.FACTORY), // Ubuntu 20.04 + new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_AARCH64_MUSL, + copyAndLoadNative("/linux_aarch64/velocity-cipher-ossl30x-musl.so"), + "OpenSSL 1.1.x (Linux aarch64, musl)", NativeVelocityCipher.FACTORY), // Alpine 3.18 new NativeCodeLoader.Variant<>(NativeConstraints.MACOS_AARCH64, copyAndLoadNative("/macos_arm64/velocity-cipher.dylib"), diff --git a/native/src/main/resources/linux_aarch64/velocity-cipher-ossl30x-musl.so b/native/src/main/resources/linux_aarch64/velocity-cipher-ossl30x-musl.so new file mode 100755 index 0000000000000000000000000000000000000000..1c6b876a63e81cbeb9745c492d3cb414eebde6ff GIT binary patch literal 71528 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 Ai2wiq literal 0 HcmV?d00001 diff --git a/native/src/main/resources/linux_aarch64/velocity-compress-musl.so b/native/src/main/resources/linux_aarch64/velocity-compress-musl.so new file mode 100755 index 0000000000000000000000000000000000000000..28137308cf6102491058512659e62bdd449e60cc GIT binary patch literal 75752 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 literal 0 HcmV?d00001 diff --git a/native/src/main/resources/linux_x86_64/velocity-compress-musl.so b/native/src/main/resources/linux_x86_64/velocity-compress-musl.so new file mode 100755 index 0000000000000000000000000000000000000000..57a25fea8a220427ac0c9d70e0fefc31a9d1bc33 GIT binary patch literal 103696 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 z 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

1cGLcub@}IQ9NL6BIaB6v@g` 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 KRnUbV)xQCXxPZO@ literal 0 HcmV?d00001 From 3c23ee75768c695a6c0afbc73f3ac11184479c61 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sat, 7 Sep 2024 13:14:04 -0400 Subject: [PATCH 09/18] Fix typo for musl crypto --- .../src/main/java/com/velocitypowered/natives/util/Natives.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/src/main/java/com/velocitypowered/natives/util/Natives.java b/native/src/main/java/com/velocitypowered/natives/util/Natives.java index 8c99fe51a..3a0bd1f22 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/Natives.java +++ b/native/src/main/java/com/velocitypowered/natives/util/Natives.java @@ -133,7 +133,7 @@ public class Natives { "OpenSSL 1.1.x (Linux aarch64)", NativeVelocityCipher.FACTORY), // Ubuntu 20.04 new NativeCodeLoader.Variant<>(NativeConstraints.LINUX_AARCH64_MUSL, copyAndLoadNative("/linux_aarch64/velocity-cipher-ossl30x-musl.so"), - "OpenSSL 1.1.x (Linux aarch64, musl)", NativeVelocityCipher.FACTORY), // Alpine 3.18 + "OpenSSL 3.x.x (Linux aarch64, musl)", NativeVelocityCipher.FACTORY), // Alpine 3.18 new NativeCodeLoader.Variant<>(NativeConstraints.MACOS_AARCH64, copyAndLoadNative("/macos_arm64/velocity-cipher.dylib"), From 0cd069ecbfa95c559d4f97282a9208f4853dbb51 Mon Sep 17 00:00:00 2001 From: Nostal Yuu Date: Thu, 12 Sep 2024 03:40:25 +0800 Subject: [PATCH 10/18] Fix exception while serializing JsonArray (#1426) --- .../proxy/protocol/packet/chat/ComponentHolder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java index a4d914634..0935f8ea6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java @@ -173,21 +173,21 @@ public class ComponentHolder { case 1://BinaryTagTypes.BYTE: byte[] bytes = new byte[jsonArray.size()]; for (int i = 0; i < bytes.length; i++) { - bytes[i] = (Byte) jsonArray.get(i).getAsNumber(); + bytes[i] = jsonArray.get(i).getAsNumber().byteValue(); } return ByteArrayBinaryTag.byteArrayBinaryTag(bytes); case 3://BinaryTagTypes.INT: int[] ints = new int[jsonArray.size()]; for (int i = 0; i < ints.length; i++) { - ints[i] = (Integer) jsonArray.get(i).getAsNumber(); + ints[i] = jsonArray.get(i).getAsNumber().intValue(); } return IntArrayBinaryTag.intArrayBinaryTag(ints); case 4://BinaryTagTypes.LONG: long[] longs = new long[jsonArray.size()]; for (int i = 0; i < longs.length; i++) { - longs[i] = (Long) jsonArray.get(i).getAsNumber(); + longs[i] = jsonArray.get(i).getAsNumber().longValue(); } return LongArrayBinaryTag.longArrayBinaryTag(longs); From 6f6d55e9e6f7c88d0781f027533221044c3aa613 Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Sat, 14 Sep 2024 13:02:08 +0200 Subject: [PATCH 11/18] [ci skip] make 'runShadow' workingDir use the 'run' directory (#1398) --- proxy/build.gradle.kts | 9 +++++++++ .../java/com/velocitypowered/proxy/VelocityServer.java | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 5e1387b06..035ff24de 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -92,6 +92,15 @@ tasks { dependsOn(configurateBuildTask) from(zipTree(configurateBuildTask.map { it.outputs.files.singleFile })) } + + runShadow { + workingDir = file("run").also(File::mkdirs) + standardInput = System.`in` + } + named("run") { + workingDir = file("run").also(File::mkdirs) + standardInput = System.`in` // Doesn't work? + } } dependencies { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 004243616..be0c8de9b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -100,6 +100,7 @@ import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.TranslationRegistry; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bstats.MetricsBase; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; @@ -263,7 +264,13 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort()); } - Metrics.VelocityMetrics.startMetrics(this, configuration.getMetrics()); + final String defaultPackage = new String( + new byte[] { 'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's' }); + if (!MetricsBase.class.getPackage().getName().startsWith(defaultPackage)) { + Metrics.VelocityMetrics.startMetrics(this, configuration.getMetrics()); + } else { + logger.warn("debug environment, metrics is disabled!"); + } } private void registerTranslations() { From 2299b78ad36d682b7c5805505e39f2691ae33fdd Mon Sep 17 00:00:00 2001 From: skbeh <60107333+skbeh@users.noreply.github.com> Date: Sat, 14 Sep 2024 11:13:57 +0000 Subject: [PATCH 12/18] fix: apply message in `PlayerChatEvent` when handling `SessionPlayerChatPacket` (#1411) Fix 1.19.3+ unsigned chat not being changed by `PlayerChatEvent`. --- .../proxy/protocol/packet/chat/session/SessionChatHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java index 0731f64ed..74b5747f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java @@ -71,7 +71,8 @@ public class SessionChatHandler implements ChatHandler invalidChange(logger, player); return null; } - return this.player.getChatBuilderFactory().builder().message(packet.message) + return this.player.getChatBuilderFactory().builder() + .message(chatResult.getMessage().orElse(packet.getMessage())) .setTimestamp(packet.timestamp) .setLastSeenMessages(newLastSeenMessages) .toServer(); From 78f6cfc26cbbb49c5b1479da3c9ea594564eb95f Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 14 Sep 2024 13:18:23 +0200 Subject: [PATCH 13/18] feat: Require any ConsoleCommandSender for the shutdown command (#1428) --- .../velocitypowered/proxy/command/builtin/ShutdownCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java index cb9e44f81..402a0ce89 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java @@ -23,6 +23,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.proxy.VelocityServer; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -43,7 +44,7 @@ public final class ShutdownCommand { */ public static BrigadierCommand command(final VelocityServer server) { return new BrigadierCommand(LiteralArgumentBuilder.literal("shutdown") - .requires(source -> source == server.getConsoleCommandSource()) + .requires(source -> source instanceof ConsoleCommandSource) .executes(context -> { server.shutdown(true); return Command.SINGLE_SUCCESS; From 4eb02c8d383dbbb566ae020b6b4f55a529075a92 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 13:45:34 -0400 Subject: [PATCH 14/18] Do not store the dimension registry for each connected server This was unused and did little more than add unnecessary memory usage. The only time we need the dimension registry is during server switching - once that is done, we no longer need the registry. --- .../connection/backend/VelocityServerConnection.java | 11 ----------- .../connection/client/ClientPlaySessionHandler.java | 2 -- 2 files changed, 13 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index edf3a9148..af3f81796 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -53,8 +53,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import net.kyori.adventure.nbt.CompoundBinaryTag; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; @@ -72,7 +70,6 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private boolean gracefulDisconnect = false; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private final Map pendingPings = new HashMap<>(); - private @MonotonicNonNull CompoundBinaryTag activeDimensionRegistry; /** * Initializes a new server connection. @@ -366,12 +363,4 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, public boolean hasCompletedJoin() { return hasCompletedJoin; } - - public CompoundBinaryTag getActiveDimensionRegistry() { - return activeDimensionRegistry; - } - - public void setActiveDimensionRegistry(CompoundBinaryTag activeDimensionRegistry) { - this.activeDimensionRegistry = activeDimensionRegistry; - } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index fed61693f..b41a24ccf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -565,8 +565,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } - destination.setActiveDimensionRegistry(joinGame.getRegistry()); // 1.16 - // Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to // track them. for (UUID serverBossBar : serverBossBars) { From ffa78d2a92a06b44b06652bd03af0c4a62ea52a6 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 17:00:15 -0400 Subject: [PATCH 15/18] Small clean-ups in event manager. --- .../velocitypowered/proxy/event/EventTypeTracker.java | 5 +++-- .../proxy/event/VelocityEventManager.java | 11 ----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java b/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java index 808e79b92..f96de135c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java @@ -33,8 +33,9 @@ class EventTypeTracker { } public Collection> getFriendsOf(final Class eventType) { - if (friends.containsKey(eventType)) { - return friends.get(eventType); + ImmutableSet> existingFriends = friends.get(eventType); + if (existingFriends != null) { + return existingFriends; } final Collection> types = getEventTypes(eventType); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java index ae0295653..f00c3877c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java @@ -156,17 +156,6 @@ public class VelocityEventManager implements EventManager { } } - enum AsyncType { - /** - * The complete event will be handled on an async thread. - */ - ALWAYS, - /** - * The event will never run async, everything is handled on the netty thread. - */ - NEVER - } - static final class HandlersCache { final HandlerRegistration[] handlers; From 4f227badc20dc30b0f6d84b5349c8809481dcbb1 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 14 May 2023 04:32:58 -0400 Subject: [PATCH 16/18] Reintroduce sync event execution to the Velocity event system This required a not-insubstantial number of bug fixes, since the sync support had bit-rotted somewhat. This PR also corrects a number of bugs. Finally. the per-plugin executor services are now used to execute all async event tasks. --- .../api/event/EventManager.java | 18 ++ .../velocitypowered/api/event/PostOrder.java | 2 +- .../velocitypowered/api/event/Subscribe.java | 34 ++-- .../velocitypowered/proxy/VelocityServer.java | 1 - .../proxy/command/VelocityCommandManager.java | 9 +- .../proxy/event/VelocityEventManager.java | 154 +++++++++++++----- .../proxy/command/CommandTestSuite.java | 11 -- .../proxy/event/EventTest.java | 146 ++++++++++++----- .../proxy/testutil/FakePluginManager.java | 29 ++-- 9 files changed, 292 insertions(+), 112 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/event/EventManager.java b/api/src/main/java/com/velocitypowered/api/event/EventManager.java index 63cae6f32..b8702ab35 100644 --- a/api/src/main/java/com/velocitypowered/api/event/EventManager.java +++ b/api/src/main/java/com/velocitypowered/api/event/EventManager.java @@ -45,10 +45,28 @@ public interface EventManager { * @param postOrder the order in which events should be posted to the handler * @param handler the handler to register * @param the event type to handle + * @deprecated use {@link #register(Object, Class, short, EventHandler)} instead */ + @Deprecated void register(Object plugin, Class eventClass, PostOrder postOrder, EventHandler handler); + /** + * Requests that the specified {@code handler} listen for events and associate it with the {@code + * plugin}. + * + *

Note that this method will register a non-asynchronous listener by default. If you want to + * use an asynchronous event handler, return {@link EventTask#async(Runnable)} from the handler.

+ * + * @param plugin the plugin to associate with the handler + * @param eventClass the class for the event handler to register + * @param postOrder the relative order in which events should be posted to the handler + * @param handler the handler to register + * @param the event type to handle + */ + void register(Object plugin, Class eventClass, short postOrder, + EventHandler handler); + /** * Fires the specified event to the event bus asynchronously. This allows Velocity to continue * servicing connections while a plugin handles a potentially long-running operation such as a diff --git a/api/src/main/java/com/velocitypowered/api/event/PostOrder.java b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java index dde8a4379..0d52ed2c0 100644 --- a/api/src/main/java/com/velocitypowered/api/event/PostOrder.java +++ b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java @@ -12,6 +12,6 @@ package com.velocitypowered.api.event; */ public enum PostOrder { - FIRST, EARLY, NORMAL, LATE, LAST + FIRST, EARLY, NORMAL, LATE, LAST, CUSTOM } diff --git a/api/src/main/java/com/velocitypowered/api/event/Subscribe.java b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java index bee71a3cb..abb96c949 100644 --- a/api/src/main/java/com/velocitypowered/api/event/Subscribe.java +++ b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java @@ -22,24 +22,38 @@ public @interface Subscribe { /** * The order events will be posted to this listener. * + * @deprecated specify the order using {@link #priority()} instead * @return the order */ + @Deprecated PostOrder order() default PostOrder.NORMAL; /** - * Whether the handler must be called asynchronously. + * The priority of this event handler. Priorities are used to determine the order in which event + * handlers are called. The higher the priority, the earlier the event handler will be called. * - *

This option currently has no effect, but in the future it will. In Velocity 3.0.0, - * all event handlers run asynchronously by default. You are encouraged to determine whether or - * not to enable it now. This option is being provided as a migration aid.

+ *

Note that due to compatibility constraints, you must specify {@link PostOrder#CUSTOM} + * in order to use this field.

* - *

If this method returns {@code true}, the method is guaranteed to be executed - * asynchronously. Otherwise, the handler may be executed on the current thread or - * asynchronously. This still means you must consider thread-safety in your - * event listeners as the "current thread" can and will be different each time.

+ * @return the priority + */ + short priority() default Short.MIN_VALUE; + + /** + * Whether the handler must be called asynchronously. By default, all event handlers are called + * asynchronously. * - *

If any method handler targeting an event type is marked with {@code true}, then every - * handler targeting that event type will be executed asynchronously.

+ *

For performance (for instance, if you use {@link EventTask#withContinuation}), you can + * optionally specify false. This option will become {@code false} by default + * in a future release of Velocity.

+ * + *

If this is {@code true}, the method is guaranteed to be executed asynchronously. Otherwise, + * the handler may be executed on the current thread or asynchronously. This still means + * you must consider thread-safety in your event listeners as the "current thread" can + * and will be different each time.

+ * + *

Note that if any method handler targeting an event type is marked with {@code true}, then + * every handler targeting that event type will be executed asynchronously.

* * @return Requires async */ diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index be0c8de9b..2a362ccc9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -540,7 +540,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { eventManager.fire(new ProxyShutdownEvent()).join(); - timedOut = !eventManager.shutdown() || timedOut; timedOut = !scheduler.shutdown() || timedOut; if (timedOut) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index a4fab9321..24fc6a059 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -49,6 +49,8 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @@ -71,6 +73,7 @@ public class VelocityCommandManager implements CommandManager { private final SuggestionsProvider suggestionsProvider; private final CommandGraphInjector injector; private final Map commandMetas; + private final ExecutorService asyncExecutor; /** * Constructs a command manager. @@ -89,6 +92,7 @@ public class VelocityCommandManager implements CommandManager { this.suggestionsProvider = new SuggestionsProvider<>(this.dispatcher, this.lock.readLock()); this.injector = new CommandGraphInjector<>(this.dispatcher, this.lock.readLock()); this.commandMetas = new ConcurrentHashMap<>(); + this.asyncExecutor = ForkJoinPool.commonPool(); // TODO: remove entirely } public void setAnnounceProxyCommands(boolean announceProxyCommands) { @@ -266,7 +270,7 @@ public class VelocityCommandManager implements CommandManager { return false; } return executeImmediately0(source, commandResult.getCommand().orElse(event.getCommand())); - }, eventManager.getAsyncExecutor()); + }, asyncExecutor); } @Override @@ -275,8 +279,7 @@ public class VelocityCommandManager implements CommandManager { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return CompletableFuture.supplyAsync( - () -> executeImmediately0(source, cmdLine), eventManager.getAsyncExecutor()); + return CompletableFuture.supplyAsync(() -> executeImmediately0(source, cmdLine), asyncExecutor); } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java index f00c3877c..4c48068cb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java @@ -25,7 +25,6 @@ import com.google.common.base.VerifyException; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.velocitypowered.api.event.Continuation; import com.velocitypowered.api.event.EventHandler; import com.velocitypowered.api.event.EventManager; @@ -38,6 +37,7 @@ import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.proxy.event.UntargetedEventHandler.EventTaskHandler; import com.velocitypowered.proxy.event.UntargetedEventHandler.VoidHandler; import com.velocitypowered.proxy.event.UntargetedEventHandler.WithContinuationHandler; +import com.velocitypowered.proxy.util.collect.Enum2IntMap; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; @@ -55,9 +55,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.BiConsumer; @@ -76,6 +73,14 @@ import org.lanternpowered.lmbda.LambdaType; */ public class VelocityEventManager implements EventManager { + private static final Enum2IntMap POST_ORDER_MAP = new Enum2IntMap.Builder<>(PostOrder.class) + .put(PostOrder.FIRST, Short.MAX_VALUE - 1) + .put(PostOrder.EARLY, Short.MAX_VALUE / 2) + .put(PostOrder.NORMAL, 0) + .put(PostOrder.LATE, Short.MIN_VALUE / 2) + .put(PostOrder.LAST, Short.MIN_VALUE + 1) + .put(PostOrder.CUSTOM, 0) + .build(); private static final Logger logger = LogManager.getLogger(VelocityEventManager.class); private static final MethodHandles.Lookup methodHandlesLookup = MethodHandles.lookup(); @@ -87,9 +92,8 @@ public class VelocityEventManager implements EventManager { LambdaType.of(WithContinuationHandler.class); private static final Comparator handlerComparator = - Comparator.comparingInt(o -> o.order); + Collections.reverseOrder(Comparator.comparingInt(o -> o.order)); - private final ExecutorService asyncExecutor; private final PluginManager pluginManager; private final ListMultimap, HandlerRegistration> handlersByType = @@ -112,9 +116,6 @@ public class VelocityEventManager implements EventManager { */ public VelocityEventManager(final PluginManager pluginManager) { this.pluginManager = pluginManager; - this.asyncExecutor = Executors - .newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactoryBuilder() - .setNameFormat("Velocity Async Event Executor - #%d").setDaemon(true).build()); } /** @@ -140,6 +141,7 @@ public class VelocityEventManager implements EventManager { final short order; final Class eventType; final EventHandler handler; + final AsyncType asyncType; /** * The instance of the {@link EventHandler} or the listener instance that was registered. @@ -147,20 +149,40 @@ public class VelocityEventManager implements EventManager { final Object instance; public HandlerRegistration(final PluginContainer plugin, final short order, - final Class eventType, final Object instance, final EventHandler handler) { + final Class eventType, final Object instance, final EventHandler handler, + final AsyncType asyncType) { this.plugin = plugin; this.order = order; this.eventType = eventType; this.instance = instance; this.handler = handler; + this.asyncType = asyncType; } } + enum AsyncType { + /** + * The event will never run async, everything is handled on the netty thread. + */ + NEVER, + /** + * The event will initially start on the thread calling the {@code fire} method, and possibly + * switch over to an async thread. + */ + SOMETIMES, + /** + * The complete event will be handled on an async thread. + */ + ALWAYS + } + static final class HandlersCache { + final AsyncType asyncType; final HandlerRegistration[] handlers; - HandlersCache(final HandlerRegistration[] handlers) { + HandlersCache(AsyncType asyncType, final HandlerRegistration[] handlers) { + this.asyncType = asyncType; this.handlers = handlers; } } @@ -183,7 +205,15 @@ public class VelocityEventManager implements EventManager { } baked.sort(handlerComparator); - return new HandlersCache(baked.toArray(new HandlerRegistration[0])); + + AsyncType asyncType = AsyncType.NEVER; + for (HandlerRegistration registration : baked) { + if (registration.asyncType.compareTo(asyncType) > 0) { + asyncType = registration.asyncType; + } + } + + return new HandlersCache(asyncType, baked.toArray(new HandlerRegistration[0])); } /** @@ -219,15 +249,17 @@ public class VelocityEventManager implements EventManager { static final class MethodHandlerInfo { final Method method; + final AsyncType asyncType; final @Nullable Class eventType; final short order; final @Nullable String errors; final @Nullable Class continuationType; - private MethodHandlerInfo(final Method method, final @Nullable Class eventType, - final short order, final @Nullable String errors, + private MethodHandlerInfo(final Method method, final AsyncType asyncType, + final @Nullable Class eventType, final short order, final @Nullable String errors, final @Nullable Class continuationType) { this.method = method; + this.asyncType = asyncType; this.eventType = eventType; this.order = order; this.errors = errors; @@ -291,17 +323,41 @@ public class VelocityEventManager implements EventManager { } } } + AsyncType asyncType = AsyncType.NEVER; + final Class returnType = method.getReturnType(); if (handlerAdapter == null) { - final Class returnType = method.getReturnType(); if (returnType != void.class && continuationType == Continuation.class) { errors.add("method return type must be void if a continuation parameter is provided"); } else if (returnType != void.class && returnType != EventTask.class) { - errors.add("method return type must be void or EventTask"); + errors.add("method return type must be void, AsyncTask, " + + "EventTask.Basic or EventTask.WithContinuation"); + } else if (returnType == EventTask.class) { + // technically, for compatibility, we *should* assume that the method must be invoked + // async, however, from examining some publicly-available plugins, developers did + // generally follow the contract and returned an EventTask only if they wanted this + // behavior. enable it for them. + asyncType = AsyncType.SOMETIMES; } + } else { + // for custom handlers, we always expect a return type of EventTask. this feature appears + // to have not been used in the wild AFAIK, so it gets the new behavior by default + asyncType = AsyncType.SOMETIMES; + } + + if (paramCount == 1 && returnType == void.class && subscribe.async()) { + // these are almost always a dead giveaway of a plugin that will need its handlers + // run async, so unless we're told otherwise, we'll assume that's the case + asyncType = AsyncType.ALWAYS; + } + + final short order; + if (subscribe.order() == PostOrder.CUSTOM) { + order = subscribe.priority(); + } else { + order = (short) POST_ORDER_MAP.get(subscribe.order()); } - final short order = (short) subscribe.order().ordinal(); final String errorsJoined = errors.isEmpty() ? null : String.join(",", errors); - collected.put(key, new MethodHandlerInfo(method, eventType, order, errorsJoined, + collected.put(key, new MethodHandlerInfo(method, asyncType, eventType, order, errorsJoined, continuationType)); } final Class superclass = targetClass.getSuperclass(); @@ -340,12 +396,29 @@ public class VelocityEventManager implements EventManager { @SuppressWarnings("unchecked") public void register(final Object plugin, final Class eventClass, final PostOrder order, final EventHandler handler) { + if (order == PostOrder.CUSTOM) { + throw new IllegalArgumentException( + "This method does not support custom post orders. Use the overload with short instead." + ); + } + register(plugin, eventClass, (short) POST_ORDER_MAP.get(order), handler, AsyncType.ALWAYS); + } + + @Override + public void register(Object plugin, Class eventClass, short postOrder, + EventHandler handler) { + register(plugin, eventClass, postOrder, handler, AsyncType.SOMETIMES); + } + + private void register(Object plugin, Class eventClass, short postOrder, + EventHandler handler, AsyncType asyncType) { final PluginContainer pluginContainer = pluginManager.ensurePluginContainer(plugin); requireNonNull(eventClass, "eventClass"); requireNonNull(handler, "handler"); final HandlerRegistration registration = new HandlerRegistration(pluginContainer, - (short) order.ordinal(), eventClass, handler, (EventHandler) handler); + postOrder, eventClass, handler, (EventHandler) handler, + AsyncType.ALWAYS); register(Collections.singletonList(registration)); } @@ -375,7 +448,7 @@ public class VelocityEventManager implements EventManager { final EventHandler handler = untargetedHandler.buildHandler(listener); registrations.add(new HandlerRegistration(pluginContainer, info.order, - info.eventType, listener, handler)); + info.eventType, listener, handler, info.asyncType)); } register(registrations); @@ -462,10 +535,13 @@ public class VelocityEventManager implements EventManager { private void fire(final @Nullable CompletableFuture future, final E event, final HandlersCache handlersCache) { - // In Velocity 1.1.0, all events were fired asynchronously. As Velocity 3.0.0 is intended to be - // largely (albeit not 100%) compatible with 1.1.x, we also fire events async. This behavior - // will go away in Velocity Polymer. - asyncExecutor.execute(() -> fire(future, event, 0, true, handlersCache.handlers)); + final HandlerRegistration registration = handlersCache.handlers[0]; + if (registration.asyncType == AsyncType.ALWAYS) { + registration.plugin.getExecutorService().execute( + () -> fire(future, event, 0, true, handlersCache.handlers)); + } else { + fire(future, event, 0, false, handlersCache.handlers); + } } private static final int TASK_STATE_DEFAULT = 0; @@ -494,6 +570,7 @@ public class VelocityEventManager implements EventManager { private final @Nullable CompletableFuture future; private final boolean currentlyAsync; private final E event; + private final Thread firedOnThread; // This field is modified via a VarHandle, so this field is used and cannot be final. @SuppressWarnings({"UnusedVariable", "FieldMayBeFinal", "FieldCanBeLocal"}) @@ -516,6 +593,7 @@ public class VelocityEventManager implements EventManager { this.event = event; this.index = index; this.currentlyAsync = currentlyAsync; + this.firedOnThread = Thread.currentThread(); } @Override @@ -526,8 +604,8 @@ public class VelocityEventManager implements EventManager { } /** - * Executes the task and returns whether the next one should be executed immediately after this - * one without scheduling. + * Executes the task and returns whether the next handler should be executed immediately + * after this one, without additional scheduling. */ boolean execute() { state = TASK_STATE_EXECUTING; @@ -569,7 +647,18 @@ public class VelocityEventManager implements EventManager { } if (!CONTINUATION_TASK_STATE.compareAndSet( this, TASK_STATE_EXECUTING, TASK_STATE_CONTINUE_IMMEDIATELY)) { - asyncExecutor.execute(() -> fire(future, event, index + 1, true, registrations)); + // We established earlier that registrations[index + 1] is a valid index. + // If we are remaining in the same thread for the next handler, fire + // the next event immediately, else fire it within the executor service + // of the plugin with the next handler. + final HandlerRegistration next = registrations[index + 1]; + final Thread currentThread = Thread.currentThread(); + if (currentThread == firedOnThread && next.asyncType != AsyncType.ALWAYS) { + fire(future, event, index + 1, currentlyAsync, registrations); + } else { + next.plugin.getExecutorService().execute(() -> + fire(future, event, index + 1, true, registrations)); + } } } @@ -595,7 +684,7 @@ public class VelocityEventManager implements EventManager { continue; } } else { - asyncExecutor.execute(continuationTask); + registration.plugin.getExecutorService().execute(continuationTask); } // fire will continue in another thread once the async task is // executed and the continuation is resumed @@ -615,13 +704,4 @@ public class VelocityEventManager implements EventManager { logger.error("Couldn't pass {} to {} {}", registration.eventType.getSimpleName(), pluginDescription.getId(), pluginDescription.getVersion().orElse(""), t); } - - public boolean shutdown() throws InterruptedException { - asyncExecutor.shutdown(); - return asyncExecutor.awaitTermination(10, TimeUnit.SECONDS); - } - - public ExecutorService getAsyncExecutor() { - return asyncExecutor; - } } \ No newline at end of file diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java index 9bd202fa4..c8e17d0f8 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java @@ -31,7 +31,6 @@ import com.velocitypowered.proxy.event.MockEventManager; import com.velocitypowered.proxy.event.VelocityEventManager; import java.util.Arrays; import java.util.Collection; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -47,16 +46,6 @@ abstract class CommandTestSuite { eventManager = new MockEventManager(); } - @AfterAll - static void afterAll() { - try { - eventManager.shutdown(); - eventManager = null; - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - @BeforeEach void setUp() { this.manager = new VelocityCommandManager(eventManager); diff --git a/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java b/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java index e05df200f..fc817f4db 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java @@ -41,20 +41,24 @@ import org.junit.jupiter.api.TestInstance; public class EventTest { public static final String CONTINUATION_TEST_THREAD_NAME = "Continuation test thread"; - private final VelocityEventManager eventManager = - new VelocityEventManager(new FakePluginManager()); + private final FakePluginManager pluginManager = new FakePluginManager(); + private final VelocityEventManager eventManager = new VelocityEventManager(pluginManager); @AfterAll void shutdown() throws Exception { - eventManager.shutdown(); + pluginManager.shutdown(); } static final class TestEvent { } + static void assertSyncThread(final Thread thread) { + assertEquals(Thread.currentThread(), thread); + } + static void assertAsyncThread(final Thread thread) { - assertTrue(thread.getName().contains("Velocity Async Event Executor")); + assertTrue(thread.getName().contains("Test Async Thread")); } static void assertContinuationThread(final Thread thread) { @@ -90,6 +94,7 @@ public class EventTest { eventManager.fire(new TestEvent()).get(); } finally { eventManager.unregisterListeners(FakePluginManager.PLUGIN_A); + eventManager.unregisterListeners(FakePluginManager.PLUGIN_B); } // Check that the order is A < B < C. @@ -119,6 +124,7 @@ public class EventTest { eventManager.fire(new TestEvent()).get(); } finally { eventManager.unregisterListeners(FakePluginManager.PLUGIN_A); + eventManager.unregisterListeners(FakePluginManager.PLUGIN_B); } // Check that the order is A < B < C. @@ -126,6 +132,26 @@ public class EventTest { assertTrue(listener2Invoked.get() < listener3Invoked.get(), "Listener C invoked before B!"); } + @Test + void testAlwaysSync() throws Exception { + final AlwaysSyncListener listener = new AlwaysSyncListener(); + handleMethodListener(listener); + assertSyncThread(listener.thread); + assertEquals(1, listener.result); + } + + static final class AlwaysSyncListener { + + @MonotonicNonNull Thread thread; + int result; + + @Subscribe(async = false) + void sync(TestEvent event) { + result++; + thread = Thread.currentThread(); + } + } + @Test void testAlwaysAsync() throws Exception { final AlwaysAsyncListener listener = new AlwaysAsyncListener(); @@ -143,7 +169,7 @@ public class EventTest { @MonotonicNonNull Thread threadC; int result; - @Subscribe + @Subscribe(async = true, order = PostOrder.EARLY) void firstAsync(TestEvent event) { result++; threadA = Thread.currentThread(); @@ -155,50 +181,93 @@ public class EventTest { return EventTask.async(() -> result++); } - @Subscribe + @Subscribe(order = PostOrder.LATE) void thirdAsync(TestEvent event) { result++; threadC = Thread.currentThread(); } } + @Test + void testSometimesAsync() throws Exception { + final SometimesAsyncListener listener = new SometimesAsyncListener(); + handleMethodListener(listener); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertAsyncThread(listener.threadC); + assertAsyncThread(listener.threadD); + assertEquals(3, listener.result); + } + + static final class SometimesAsyncListener { + + @MonotonicNonNull Thread threadA; + @MonotonicNonNull Thread threadB; + @MonotonicNonNull Thread threadC; + @MonotonicNonNull Thread threadD; + int result; + + @Subscribe(order = PostOrder.EARLY, async = false) + void notAsync(TestEvent event) { + result++; + threadA = Thread.currentThread(); + } + + @Subscribe + EventTask notAsyncUntilTask(TestEvent event) { + threadB = Thread.currentThread(); + return EventTask.async(() -> { + threadC = Thread.currentThread(); + result++; + }); + } + + @Subscribe(order = PostOrder.LATE, async = false) + void stillAsyncAfterTask(TestEvent event) { + threadD = Thread.currentThread(); + result++; + } + } + @Test void testContinuation() throws Exception { final ContinuationListener listener = new ContinuationListener(); handleMethodListener(listener); - assertAsyncThread(listener.thread1); - assertAsyncThread(listener.thread2); - assertContinuationThread(listener.thread2Custom); - assertAsyncThread(listener.thread3); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertAsyncThread(listener.threadC); assertEquals(2, listener.value.get()); } static final class ContinuationListener { - @MonotonicNonNull Thread thread1; - @MonotonicNonNull Thread thread2; - @MonotonicNonNull Thread thread2Custom; - @MonotonicNonNull Thread thread3; + @MonotonicNonNull Thread threadA; + @MonotonicNonNull Thread threadB; + @MonotonicNonNull Thread threadC; final AtomicInteger value = new AtomicInteger(); @Subscribe(order = PostOrder.EARLY) EventTask continuation(TestEvent event) { - thread1 = Thread.currentThread(); + threadA = Thread.currentThread(); return EventTask.withContinuation(continuation -> { value.incrementAndGet(); - thread2 = Thread.currentThread(); + threadB = Thread.currentThread(); new Thread(() -> { - thread2Custom = Thread.currentThread(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } value.incrementAndGet(); continuation.resume(); - }, CONTINUATION_TEST_THREAD_NAME).start(); + }).start(); }); } @Subscribe(order = PostOrder.LATE) void afterContinuation(TestEvent event) { - thread3 = Thread.currentThread(); + threadC = Thread.currentThread(); } } @@ -207,9 +276,9 @@ public class EventTest { final ResumeContinuationImmediatelyListener listener = new ResumeContinuationImmediatelyListener(); handleMethodListener(listener); - assertAsyncThread(listener.threadA); - assertAsyncThread(listener.threadB); - assertAsyncThread(listener.threadC); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertSyncThread(listener.threadC); assertEquals(2, listener.result); } @@ -241,42 +310,44 @@ public class EventTest { void testContinuationParameter() throws Exception { final ContinuationParameterListener listener = new ContinuationParameterListener(); handleMethodListener(listener); - assertAsyncThread(listener.thread1); - assertAsyncThread(listener.thread2); - assertContinuationThread(listener.thread2Custom); - assertAsyncThread(listener.thread3); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertAsyncThread(listener.threadC); assertEquals(3, listener.result.get()); } static final class ContinuationParameterListener { - @MonotonicNonNull Thread thread1; - @MonotonicNonNull Thread thread2; - @MonotonicNonNull Thread thread2Custom; - @MonotonicNonNull Thread thread3; + @MonotonicNonNull Thread threadA; + @MonotonicNonNull Thread threadB; + @MonotonicNonNull Thread threadC; final AtomicInteger result = new AtomicInteger(); @Subscribe void resume(TestEvent event, Continuation continuation) { - thread1 = Thread.currentThread(); + threadA = Thread.currentThread(); result.incrementAndGet(); continuation.resume(); } @Subscribe(order = PostOrder.LATE) void resumeFromCustomThread(TestEvent event, Continuation continuation) { - thread2 = Thread.currentThread(); + threadB = Thread.currentThread(); new Thread(() -> { - thread2Custom = Thread.currentThread(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } result.incrementAndGet(); continuation.resume(); - }, CONTINUATION_TEST_THREAD_NAME).start(); + }).start(); } @Subscribe(order = PostOrder.LAST) void afterCustomThread(TestEvent event, Continuation continuation) { - thread3 = Thread.currentThread(); + threadC = Thread.currentThread(); result.incrementAndGet(); continuation.resume(); } @@ -328,8 +399,7 @@ public class EventTest { + "the second is the fancy continuation"); } }, - new TypeToken>() { - }, + new TypeToken>() {}, invokeFunction -> (instance, event) -> EventTask.withContinuation(continuation -> invokeFunction.accept(instance, event, new FancyContinuationImpl(continuation)) @@ -349,4 +419,4 @@ public class EventTest { continuation.resume(); } } -} \ No newline at end of file +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java index 04ba3867d..b1bc1af72 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.testutil; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; @@ -25,7 +26,7 @@ import java.nio.file.Path; import java.util.Collection; import java.util.Optional; import java.util.concurrent.ExecutorService; -import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Executors; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -36,15 +37,19 @@ public class FakePluginManager implements PluginManager { public static final Object PLUGIN_A = new Object(); public static final Object PLUGIN_B = new Object(); - private static final PluginContainer PC_A = new FakePluginContainer("a", PLUGIN_A); - private static final PluginContainer PC_B = new FakePluginContainer("b", PLUGIN_B); + private final PluginContainer containerA = new FakePluginContainer("a", PLUGIN_A); + private final PluginContainer containerB = new FakePluginContainer("b", PLUGIN_B); + + private ExecutorService service = Executors.newCachedThreadPool( + new ThreadFactoryBuilder().setNameFormat("Test Async Thread").setDaemon(true).build() + ); @Override public @NonNull Optional fromInstance(@NonNull Object instance) { if (instance == PLUGIN_A) { - return Optional.of(PC_A); + return Optional.of(containerA); } else if (instance == PLUGIN_B) { - return Optional.of(PC_B); + return Optional.of(containerB); } else { return Optional.empty(); } @@ -54,9 +59,9 @@ public class FakePluginManager implements PluginManager { public @NonNull Optional getPlugin(@NonNull String id) { switch (id) { case "a": - return Optional.of(PC_A); + return Optional.of(containerA); case "b": - return Optional.of(PC_B); + return Optional.of(containerB); default: return Optional.empty(); } @@ -64,7 +69,7 @@ public class FakePluginManager implements PluginManager { @Override public @NonNull Collection getPlugins() { - return ImmutableList.of(PC_A, PC_B); + return ImmutableList.of(containerA, containerB); } @Override @@ -77,16 +82,18 @@ public class FakePluginManager implements PluginManager { throw new UnsupportedOperationException(); } - private static class FakePluginContainer implements PluginContainer { + public void shutdown() { + this.service.shutdownNow(); + } + + private class FakePluginContainer implements PluginContainer { private final String id; private final Object instance; - private final ExecutorService service; private FakePluginContainer(String id, Object instance) { this.id = id; this.instance = instance; - this.service = ForkJoinPool.commonPool(); } @Override From 2016d1482f185daf4b92b6775fb4a8d2ef1e0da3 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 20:22:55 -0400 Subject: [PATCH 17/18] Deprecate anonymous command registrations --- .../api/command/CommandManager.java | 4 + .../velocitypowered/proxy/VelocityServer.java | 55 ++++++++++-- .../proxy/command/VelocityCommandManager.java | 70 +++++++++++---- .../proxy/command/VelocityCommands.java | 90 ++++++++++++++++++- .../VelocityArgumentCommandNode.java | 10 +++ .../VelocityBrigadierCommandWrapper.java | 67 ++++++++++++++ .../proxy/command/builtin/GlistCommand.java | 9 +- .../proxy/command/builtin/SendCommand.java | 9 +- .../registrar/BrigadierCommandRegistrar.java | 6 +- .../registrar/InvocableCommandRegistrar.java | 5 +- .../proxy/plugin/VelocityPluginManager.java | 7 +- .../plugin/virtual/VelocityVirtualPlugin.java | 29 ++++++ .../proxy/command/BrigadierCommandTests.java | 2 +- .../proxy/command/CommandTestSuite.java | 3 +- .../proxy/testutil/FakePluginManager.java | 9 +- 15 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java index 257fc9e64..ad3893738 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -45,7 +45,9 @@ public interface CommandManager { * @throws IllegalArgumentException if one of the given aliases is already registered, or * the given command does not implement a registrable {@link Command} subinterface * @see Command for a list of registrable Command subinterfaces + * @deprecated use {@link #register(CommandMeta, Command)} instead with a plugin specified */ + @Deprecated default void register(String alias, Command command, String... otherAliases) { register(metaBuilder(alias).aliases(otherAliases).build(), command); } @@ -55,7 +57,9 @@ public interface CommandManager { * * @param command the command to register * @throws IllegalArgumentException if the node alias is already registered + * @deprecated use {@link #register(CommandMeta, Command)} instead with a plugin specified */ + @Deprecated void register(BrigadierCommand command); /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 2a362ccc9..2563f984f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -22,11 +22,13 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyReloadEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; @@ -52,6 +54,9 @@ import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.event.VelocityEventManager; import com.velocitypowered.proxy.network.ConnectionManager; import com.velocitypowered.proxy.plugin.VelocityPluginManager; +import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; +import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.protocol.util.GameProfileSerializer; @@ -77,6 +82,7 @@ import java.nio.file.Path; import java.security.KeyPair; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -111,6 +117,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; */ public class VelocityServer implements ProxyServer, ForwardingAudience { + public static final String VELOCITY_URL = "https://velocitypowered.com"; + private static final Logger logger = LogManager.getLogger(VelocityServer.class); public static final Gson GENERAL_GSON = new GsonBuilder() .registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE) @@ -163,7 +171,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { VelocityServer(final ProxyOptions options) { pluginManager = new VelocityPluginManager(this); eventManager = new VelocityEventManager(pluginManager); - commandManager = new VelocityCommandManager(eventManager); + commandManager = new VelocityCommandManager(eventManager, pluginManager); scheduler = new VelocityScheduler(pluginManager); console = new VelocityConsole(this); cm = new ConnectionManager(this); @@ -200,6 +208,16 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { return new ProxyVersion(implName, implVendor, implVersion); } + private VelocityPluginContainer createVirtualPlugin() { + ProxyVersion version = getVersion(); + PluginDescription description = new VelocityPluginDescription( + "velocity", version.getName(), version.getVersion(), "The Velocity proxy", + VELOCITY_URL, ImmutableList.of(version.getVendor()), Collections.emptyList(), null); + VelocityPluginContainer container = new VelocityPluginContainer(description); + container.setInstance(VelocityVirtualPlugin.INSTANCE); + return container; + } + @Override public VelocityCommandManager getCommandManager() { return commandManager; @@ -214,6 +232,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { void start() { logger.info("Booting up {} {}...", getVersion().getName(), getVersion().getVersion()); console.setupStreams(); + pluginManager.registerPlugin(this.createVirtualPlugin()); registerTranslations(); @@ -222,11 +241,35 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { cm.logChannelInformation(); // Initialize commands first - commandManager.register(VelocityCommand.create(this)); - commandManager.register(CallbackCommand.create()); - commandManager.register(ServerCommand.create(this)); - commandManager.register("shutdown", ShutdownCommand.command(this), - "end", "stop"); + final BrigadierCommand velocityParentCommand = VelocityCommand.create(this); + commandManager.register( + commandManager.metaBuilder(velocityParentCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + velocityParentCommand + ); + final BrigadierCommand callbackCommand = CallbackCommand.create(); + commandManager.register( + commandManager.metaBuilder(callbackCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + velocityParentCommand + ); + final BrigadierCommand serverCommand = ServerCommand.create(this); + commandManager.register( + commandManager.metaBuilder(serverCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + serverCommand + ); + final BrigadierCommand shutdownCommand = ShutdownCommand.command(this); + commandManager.register( + commandManager.metaBuilder(shutdownCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .aliases("end", "stop") + .build(), + shutdownCommand + ); new GlistCommand(this).register(); new SendCommand(this).register(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index 24fc6a059..f8bb39f07 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -20,6 +20,7 @@ package com.velocitypowered.proxy.command; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import com.google.common.util.concurrent.MoreExecutors; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.Message; import com.mojang.brigadier.ParseResults; @@ -37,11 +38,15 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.VelocityBrigadierMessage; import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.command.PostCommandInvocationEvent; +import com.velocitypowered.api.plugin.PluginManager; +import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import com.velocitypowered.proxy.command.registrar.BrigadierCommandRegistrar; import com.velocitypowered.proxy.command.registrar.CommandRegistrar; import com.velocitypowered.proxy.command.registrar.RawCommandRegistrar; import com.velocitypowered.proxy.command.registrar.SimpleCommandRegistrar; import com.velocitypowered.proxy.event.VelocityEventManager; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; +import io.netty.util.concurrent.FastThreadLocalThread; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -49,8 +54,7 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Executor; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @@ -73,14 +77,16 @@ public class VelocityCommandManager implements CommandManager { private final SuggestionsProvider suggestionsProvider; private final CommandGraphInjector injector; private final Map commandMetas; - private final ExecutorService asyncExecutor; + private final PluginManager pluginManager; /** * Constructs a command manager. * * @param eventManager the event manager */ - public VelocityCommandManager(final VelocityEventManager eventManager) { + public VelocityCommandManager(final VelocityEventManager eventManager, + PluginManager pluginManager) { + this.pluginManager = pluginManager; this.lock = new ReentrantReadWriteLock(); this.dispatcher = new CommandDispatcher<>(); this.eventManager = Preconditions.checkNotNull(eventManager); @@ -92,7 +98,6 @@ public class VelocityCommandManager implements CommandManager { this.suggestionsProvider = new SuggestionsProvider<>(this.dispatcher, this.lock.readLock()); this.injector = new CommandGraphInjector<>(this.dispatcher, this.lock.readLock()); this.commandMetas = new ConcurrentHashMap<>(); - this.asyncExecutor = ForkJoinPool.commonPool(); // TODO: remove entirely } public void setAnnounceProxyCommands(boolean announceProxyCommands) { @@ -222,16 +227,13 @@ public class VelocityCommandManager implements CommandManager { return eventManager.fire(new CommandExecuteEvent(source, cmdLine)); } - private boolean executeImmediately0(final CommandSource source, final String cmdLine) { + private boolean executeImmediately0(final CommandSource source, final ParseResults parsed) { Preconditions.checkNotNull(source, "source"); - Preconditions.checkNotNull(cmdLine, "cmdLine"); - final String normalizedInput = VelocityCommands.normalizeInput(cmdLine, true); CommandResult result = CommandResult.EXCEPTION; try { // The parse can fail if the requirement predicates throw - final ParseResults parse = this.parse(normalizedInput, source); - boolean executed = dispatcher.execute(parse) != BrigadierCommand.FORWARD; + boolean executed = dispatcher.execute(parsed) != BrigadierCommand.FORWARD; result = executed ? CommandResult.EXECUTED : CommandResult.FORWARDED; return executed; } catch (final CommandSyntaxException e) { @@ -253,9 +255,9 @@ public class VelocityCommandManager implements CommandManager { } } catch (final Throwable e) { // Ugly, ugly swallowing of everything Throwable, because plugins are naughty. - throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e); + throw new RuntimeException("Unable to invoke command " + parsed.getReader().getString() + "for " + source, e); } finally { - eventManager.fireAndForget(new PostCommandInvocationEvent(source, cmdLine, result)); + eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result)); } } @@ -264,13 +266,17 @@ public class VelocityCommandManager implements CommandManager { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return callCommandEvent(source, cmdLine).thenApplyAsync(event -> { + return callCommandEvent(source, cmdLine).thenComposeAsync(event -> { CommandExecuteEvent.CommandResult commandResult = event.getResult(); if (commandResult.isForwardToServer() || !commandResult.isAllowed()) { - return false; + return CompletableFuture.completedFuture(false); } - return executeImmediately0(source, commandResult.getCommand().orElse(event.getCommand())); - }, asyncExecutor); + final ParseResults parsed = this.parse( + commandResult.getCommand().orElse(cmdLine), source); + return CompletableFuture.supplyAsync( + () -> executeImmediately0(source, parsed), this.getAsyncExecutor(parsed) + ); + }, figureAsyncExecutorForParsing()); } @Override @@ -279,7 +285,13 @@ public class VelocityCommandManager implements CommandManager { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return CompletableFuture.supplyAsync(() -> executeImmediately0(source, cmdLine), asyncExecutor); + return CompletableFuture.supplyAsync( + () -> this.parse(cmdLine, source), figureAsyncExecutorForParsing() + ).thenCompose( + parsed -> CompletableFuture.supplyAsync( + () -> executeImmediately0(source, parsed), this.getAsyncExecutor(parsed) + ) + ); } /** @@ -327,9 +339,10 @@ public class VelocityCommandManager implements CommandManager { * @return the parse results */ private ParseResults parse(final String input, final CommandSource source) { + final String normalizedInput = VelocityCommands.normalizeInput(input, true); lock.readLock().lock(); try { - return dispatcher.parse(input, source); + return dispatcher.parse(normalizedInput, source); } finally { lock.readLock().unlock(); } @@ -373,4 +386,25 @@ public class VelocityCommandManager implements CommandManager { public CommandGraphInjector getInjector() { return injector; } + + private Executor getAsyncExecutor(ParseResults parse) { + Object registrant; + if (parse.getContext().getCommand() instanceof VelocityBrigadierCommandWrapper vbcw) { + registrant = vbcw.registrant() == null ? VelocityVirtualPlugin.INSTANCE : vbcw.registrant(); + } else { + registrant = VelocityVirtualPlugin.INSTANCE; + } + return pluginManager.ensurePluginContainer(registrant).getExecutorService(); + } + + private Executor figureAsyncExecutorForParsing() { + final Thread thread = Thread.currentThread(); + if (thread instanceof FastThreadLocalThread) { + // we *never* want to block the Netty event loop, so use the async executor + return pluginManager.ensurePluginContainer(VelocityVirtualPlugin.INSTANCE).getExecutorService(); + } else { + // it's some other thread that isn't a Netty event loop thread. direct execution it is! + return MoreExecutors.directExecutor(); + } + } } \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java index 2f10ed25b..3c2ad1991 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java @@ -24,6 +24,7 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.ParsedArgument; import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; @@ -32,6 +33,7 @@ import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.InvocableCommand; import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode; +import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import java.util.List; import java.util.Locale; import java.util.Map; @@ -44,6 +46,59 @@ import org.checkerframework.checker.nullness.qual.Nullable; */ public final class VelocityCommands { + // Wrapping + + /** + * Walks the command node tree and wraps all {@link Command} instances in a {@link VelocityBrigadierCommandWrapper}, + * to indicate the plugin that registered the command. This also has the side effect of cloning + * the command node tree. + * + * @param delegate the command node to wrap + * @param registrant the plugin that registered the command + * @return the wrapped command node + */ + public static CommandNode wrap(final CommandNode delegate, + final @Nullable Object registrant) { + Preconditions.checkNotNull(delegate, "delegate"); + if (registrant == null) { + // the registrant is null if the `plugin` was absent when we try to register the command + return delegate; + } + + com.mojang.brigadier.Command maybeCommand = delegate.getCommand(); + if (maybeCommand != null && !(maybeCommand instanceof VelocityBrigadierCommandWrapper)) { + maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant); + } + + if (delegate instanceof LiteralCommandNode lcn) { + var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true); + literalBuilder.executes(maybeCommand); + // we also need to wrap any children + for (final CommandNode child : delegate.getChildren()) { + literalBuilder.then(wrap(child, registrant)); + } + if (delegate.getRedirect() != null) { + literalBuilder.redirect(wrap(delegate.getRedirect(), registrant)); + } + return literalBuilder.build(); + } else if (delegate instanceof VelocityArgumentCommandNode vacn) { + return vacn.withCommand(maybeCommand) + .withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null); + } else if (delegate instanceof ArgumentCommandNode) { + var argBuilder = delegate.createBuilder().executes(maybeCommand); + // we also need to wrap any children + for (final CommandNode child : delegate.getChildren()) { + argBuilder.then(wrap(child, registrant)); + } + if (delegate.getRedirect() != null) { + argBuilder.redirect(wrap(delegate.getRedirect(), registrant)); + } + return argBuilder.build(); + } else { + throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass()); + } + } + // Normalization /** @@ -135,6 +190,33 @@ public final class VelocityCommands { */ public static LiteralCommandNode shallowCopy( final LiteralCommandNode original, final String newName) { + return shallowCopy(original, newName, original.getCommand()); + } + + /** + * Creates a copy of the given literal with the specified name. + * + * @param original the literal node to copy + * @param newName the name of the returned literal node + * @param newCommand the new command to set on the copied node + * @return a copy of the literal with the given name + */ + private static LiteralCommandNode shallowCopy( + final LiteralCommandNode original, final String newName, + final com.mojang.brigadier.Command newCommand) { + return shallowCopyAsBuilder(original, newName, false).executes(newCommand).build(); + } + + /** + * Creates a copy of the given literal with the specified name. + * + * @param original the literal node to copy + * @param newName the name of the returned literal node + * @return a copy of the literal with the given name + */ + private static LiteralArgumentBuilder shallowCopyAsBuilder( + final LiteralCommandNode original, final String newName, + final boolean skipChildren) { // Brigadier resolves the redirect of a node if further input can be parsed. // Let be a literal node having a redirect to a literal. Then, // the context returned by CommandDispatcher#parseNodes when given the input @@ -150,10 +232,12 @@ public final class VelocityCommands { .requiresWithContext(original.getContextRequirement()) .forward(original.getRedirect(), original.getRedirectModifier(), original.isFork()) .executes(original.getCommand()); - for (final CommandNode child : original.getChildren()) { - builder.then(child); + if (!skipChildren) { + for (final CommandNode child : original.getChildren()) { + builder.then(child); + } } - return builder.build(); + return builder; } // Arguments node diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java index 9faa29294..03ba6d35e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java @@ -93,6 +93,16 @@ public class VelocityArgumentCommandNode extends ArgumentCommandNode withCommand(Command command) { + return new VelocityArgumentCommandNode<>(getName(), type, command, getRequirement(), + getContextRequirement(), getRedirect(), getRedirectModifier(), isFork(), getCustomSuggestions()); + } + + public VelocityArgumentCommandNode withRedirect(CommandNode target) { + return new VelocityArgumentCommandNode<>(getName(), type, getCommand(), getRequirement(), + getContextRequirement(), target, getRedirectModifier(), isFork(), getCustomSuggestions()); + } + @Override public boolean isValidInput(final String input) { return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java new file mode 100644 index 000000000..8502134cc --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package com.velocitypowered.proxy.command.brigadier; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.velocitypowered.api.command.CommandSource; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Wraps a Brigadier command to allow us to track the registrant. + */ +public class VelocityBrigadierCommandWrapper implements Command { + + private final Command delegate; + private final Object registrant; + + private VelocityBrigadierCommandWrapper(Command delegate, Object registrant) { + this.delegate = delegate; + this.registrant = registrant; + } + + /** + * Transforms the given command into a {@code VelocityBrigadierCommandWrapper} if the registrant + * is not null and if the command is not already wrapped. + * + * @param delegate the command to wrap + * @param registrant the registrant of the command + * @return the wrapped command, if necessary + */ + public static Command wrap(Command delegate, @Nullable Object registrant) { + if (registrant == null) { + // nothing to wrap + return delegate; + } + if (delegate instanceof VelocityBrigadierCommandWrapper) { + // already wrapped + return delegate; + } + return new VelocityBrigadierCommandWrapper(delegate, registrant); + } + + @Override + public int run(CommandContext context) throws CommandSyntaxException { + return delegate.run(context); + } + + public Object registrant() { + return registrant; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java index 2cf8c92d2..2b73dcc13 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java @@ -31,6 +31,7 @@ import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import java.util.List; import java.util.Optional; import net.kyori.adventure.text.Component; @@ -80,7 +81,13 @@ public class GlistCommand { .executes(this::serverCount) .build(); rootNode.then(serverNode); - server.getCommandManager().register(new BrigadierCommand(rootNode)); + final BrigadierCommand command = new BrigadierCommand(rootNode); + server.getCommandManager().register( + server.getCommandManager().metaBuilder(command) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + command + ); } private int totalCount(final CommandContext context) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java index 626131daa..d0df03488 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java @@ -30,6 +30,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import java.util.Objects; import java.util.Optional; import net.kyori.adventure.text.Component; @@ -96,7 +97,13 @@ public class SendCommand { .build(); playerNode.then(serverNode); rootNode.then(playerNode.build()); - server.getCommandManager().register(new BrigadierCommand(rootNode.build())); + final BrigadierCommand command = new BrigadierCommand(rootNode); + server.getCommandManager().register( + server.getCommandManager().metaBuilder(command) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + command + ); } private int usage(final CommandContext context) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java index bc8d02de4..74d3f30ee 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java @@ -40,17 +40,19 @@ public final class BrigadierCommandRegistrar extends AbstractCommandRegistrar
literal = command.getNode(); + final LiteralCommandNode wrapped = + (LiteralCommandNode) VelocityCommands.wrap(literal, meta.getPlugin()); final String primaryAlias = literal.getName(); if (VelocityCommands.isValidAlias(primaryAlias)) { // Register directly without copying - this.register(literal); + this.register(wrapped); } for (final String alias : meta.getAliases()) { if (primaryAlias.equals(alias)) { continue; } - this.register(literal, alias); + this.register(wrapped, alias); } // Brigadier commands don't support hinting, ignore diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java index a526fe3de..380f45e39 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java @@ -32,6 +32,7 @@ import com.velocitypowered.api.command.InvocableCommand; import com.velocitypowered.proxy.command.VelocityCommandMeta; import com.velocitypowered.proxy.command.VelocityCommands; import com.velocitypowered.proxy.command.brigadier.VelocityArgumentBuilder; +import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import com.velocitypowered.proxy.command.invocation.CommandInvocationFactory; import java.util.Iterator; import java.util.concurrent.locks.Lock; @@ -76,11 +77,11 @@ abstract class InvocableCommandRegistrar, final I invocation = invocationFactory.create(context); return command.hasPermission(invocation); }; - final Command callback = context -> { + final Command callback = VelocityBrigadierCommandWrapper.wrap(context -> { final I invocation = invocationFactory.create(context); command.execute(invocation); return 1; // handled - }; + }, meta.getPlugin()); final LiteralCommandNode literal = LiteralArgumentBuilder .literal(alias) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index 0af477c42..6bd0e0085 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -68,7 +68,12 @@ public class VelocityPluginManager implements PluginManager { this.server = checkNotNull(server, "server"); } - private void registerPlugin(PluginContainer plugin) { + /** + * Registers a plugin with the plugin manager. + * + * @param plugin the plugin to register + */ + public void registerPlugin(PluginContainer plugin) { pluginsById.put(plugin.getDescription().getId(), plugin); Optional instance = plugin.getInstance(); instance.ifPresent(o -> pluginInstances.put(o, plugin)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java new file mode 100644 index 000000000..08e4f57c3 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package com.velocitypowered.proxy.plugin.virtual; + +/** + * A singleton plugin object that represents the Velocity proxy itself. + */ +public class VelocityVirtualPlugin { + @SuppressWarnings("InstantiationOfUtilityClass") + public static final VelocityVirtualPlugin INSTANCE = new VelocityVirtualPlugin(); + + private VelocityVirtualPlugin() { + } +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java b/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java index f4110ba52..abbfbc422 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java @@ -230,7 +230,7 @@ public class BrigadierCommandTests extends CommandTestSuite { final Exception wrapper = assertThrows(CompletionException.class, () -> manager.executeAsync(source, "hello").join()); - assertSame(expected, wrapper.getCause().getCause()); + assertSame(expected, wrapper.getCause()); } // Suggestions diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java index c8e17d0f8..66e49838a 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java @@ -29,6 +29,7 @@ import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.event.MockEventManager; import com.velocitypowered.proxy.event.VelocityEventManager; +import com.velocitypowered.proxy.testutil.FakePluginManager; import java.util.Arrays; import java.util.Collection; import org.junit.jupiter.api.BeforeAll; @@ -48,7 +49,7 @@ abstract class CommandTestSuite { @BeforeEach void setUp() { - this.manager = new VelocityCommandManager(eventManager); + this.manager = new VelocityCommandManager(eventManager, new FakePluginManager()); } final void assertHandled(final String input) { diff --git a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java index b1bc1af72..7992ac52e 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import java.nio.file.Path; import java.util.Collection; import java.util.Optional; @@ -39,6 +40,8 @@ public class FakePluginManager implements PluginManager { private final PluginContainer containerA = new FakePluginContainer("a", PLUGIN_A); private final PluginContainer containerB = new FakePluginContainer("b", PLUGIN_B); + private final PluginContainer containerVelocity = new FakePluginContainer("velocity", + VelocityVirtualPlugin.INSTANCE); private ExecutorService service = Executors.newCachedThreadPool( new ThreadFactoryBuilder().setNameFormat("Test Async Thread").setDaemon(true).build() @@ -50,6 +53,8 @@ public class FakePluginManager implements PluginManager { return Optional.of(containerA); } else if (instance == PLUGIN_B) { return Optional.of(containerB); + } else if (instance == VelocityVirtualPlugin.INSTANCE) { + return Optional.of(containerVelocity); } else { return Optional.empty(); } @@ -62,6 +67,8 @@ public class FakePluginManager implements PluginManager { return Optional.of(containerA); case "b": return Optional.of(containerB); + case "velocity": + return Optional.of(containerVelocity); default: return Optional.empty(); } @@ -69,7 +76,7 @@ public class FakePluginManager implements PluginManager { @Override public @NonNull Collection getPlugins() { - return ImmutableList.of(containerA, containerB); + return ImmutableList.of(containerVelocity, containerA, containerB); } @Override From b66aa3fb4e6d2350c85523685c5c2420c029ade8 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 20:22:55 -0400 Subject: [PATCH 18/18] Bump to Velocity 3.4.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cd9c67796..55c2ed10e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group=com.velocitypowered -version=3.3.0-SNAPSHOT +version=3.4.0-SNAPSHOT