Fix Java 15 (#1025)

- Implemented a fix for the incompatibility with Java 15. This incompatibility was caused by the fact that the lambda generated in the NMS.NetworkManager is a hidden class in J15. Starting in Java 15, final fields in hidden classes can no longer be modified regardless of the 'accessible' flag. (see https://openjdk.java.net/jeps/371 "Using a hidden class", point 3). To circumvent this issue, this retrieves the data from the existing fields in the hidden class (a runnable or a callable) other than the packet. It then retrieves the constructor of the hidden class and instantiates it using the previously-retrieved data and the modified packet instance (this code is only used if the packet instance changed).
- Introduced a new ObjectReconstructor class that does all the fields/constructor discovering/accessing etc. The Runnable and Callable methods each get one instance of this class so that we can avoid having to get the fields/constructors and set them accessible every time we want to replace a packet.

Co-authored-by: Mark Vainomaa <mikroskeem@mikroskeem.eu>
This commit is contained in:
PimvanderLoos 2020-12-23 20:57:16 +01:00 committed by GitHub
parent 7bac4ec634
commit bbb053aa4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 144 additions and 15 deletions

View File

@ -34,6 +34,8 @@ import com.comphenix.protocol.utility.MinecraftFields;
import com.comphenix.protocol.utility.MinecraftMethods;
import com.comphenix.protocol.utility.MinecraftProtocolVersion;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.ObjectReconstructor;
import com.comphenix.protocol.wrappers.Pair;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
@ -48,6 +50,7 @@ import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
@ -55,8 +58,8 @@ import java.net.SocketAddress;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Represents a channel injector.
* @author Kristian
@ -88,8 +91,23 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
} catch (Exception ex) {
throw new RuntimeException("Encountered an error caused by a reload! Please properly restart your server!", ex);
}
Method hiddenClassMethod = null;
try {
if (Float.parseFloat(System.getProperty("java.class.version")) >= 59) {
hiddenClassMethod = Class.class.getMethod("isHidden");
}
} catch (NoSuchMethodException ignored) {
}
IS_HIDDEN_CLASS = hiddenClassMethod;
}
// Starting in Java 15 (59), the lambdas are hidden classes and we cannot use reflection to update
// the values anymore. Instead, the object will have to be reconstructed.
private static final Map<Class<?>, ObjectReconstructor<?>> RECONSTRUCTORS = new ConcurrentHashMap<>();
private static final Method IS_HIDDEN_CLASS;
// Saved accessors
private Method decodeBuffer;
private Method encodeBuffer;
@ -295,18 +313,18 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
@Override
protected <T> Callable<T> onMessageScheduled(final Callable<T> callable, FieldAccessor packetAccessor) {
final PacketEvent event = handleScheduled(callable, packetAccessor);
Pair<Callable<T>, PacketEvent> handled = handleScheduled(callable, packetAccessor);
// Handle cancelled events
if (event != null && event.isCancelled())
if (handled.getSecond() != null && handled.getSecond().isCancelled())
return null;
return () -> {
T result;
// This field must only be updated in the pipeline thread
currentEvent = event;
result = callable.call();
currentEvent = handled.getSecond();
result = handled.getFirst().call();
currentEvent = null;
return result;
};
@ -314,20 +332,20 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
@Override
protected Runnable onMessageScheduled(final Runnable runnable, FieldAccessor packetAccessor) {
final PacketEvent event = handleScheduled(runnable, packetAccessor);
Pair<Runnable, PacketEvent> handled = handleScheduled(runnable, packetAccessor);
// Handle cancelled events
if (event != null && event.isCancelled())
if (handled.getSecond() != null && handled.getSecond().isCancelled())
return null;
return () -> {
currentEvent = event;
runnable.run();
currentEvent = handled.getSecond();
handled.getFirst().run();
currentEvent = null;
};
}
PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
<T> Pair<T, PacketEvent> handleScheduled(T instance, FieldAccessor accessor) {
// Let the filters handle this packet
Object original = accessor.get(instance);
@ -338,9 +356,9 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
if (marker != null) {
PacketEvent result = new PacketEvent(ChannelInjector.class);
result.setNetworkMarker(marker);
return result;
return new Pair<>(instance, result);
} else {
return BYPASSED_PACKET;
return new Pair<>(instance, BYPASSED_PACKET);
}
}
@ -350,11 +368,12 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
// Change packet to be scheduled
if (original != changed) {
accessor.set(instance, changed);
instance = (T) (isHiddenClass(instance.getClass()) ?
updatePacketMessageReconstruct(instance, changed, accessor) :
updatePacketMessageSetReflection(instance, changed, accessor));
}
}
return event != null ? event : BYPASSED_PACKET;
return new Pair<>(instance, event != null ? event : BYPASSED_PACKET);
}
});
@ -363,6 +382,30 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
}
}
/**
* Changes the packet in a packet message using a {@link FieldAccessor}.
*/
private static Object updatePacketMessageSetReflection(Object instance, Object newPacket, FieldAccessor accessor) {
accessor.set(instance, newPacket);
return instance;
}
/**
* Changes the packet in a packet message using a {@link ObjectReconstructor}.
*/
private static Object updatePacketMessageReconstruct(Object instance, Object newPacket, FieldAccessor accessor) {
final ObjectReconstructor<?> objectReconstructor =
RECONSTRUCTORS.computeIfAbsent(instance.getClass(), ObjectReconstructor::new);
final Object[] values = objectReconstructor.getValues(instance);
final Field[] fields = objectReconstructor.getFields();
for (int idx = 0; idx < fields.length; ++idx)
if (fields[idx].equals(accessor.getField()))
values[idx] = newPacket;
return objectReconstructor.reconstruct(values);
}
/**
* Determine if the given object is a compressor or decompressor.
* @param handler - object to test.
@ -943,4 +986,15 @@ public class ChannelInjector extends ByteToMessageDecoder implements Injector {
public Channel getChannel() {
return originalChannel;
}
private static boolean isHiddenClass(Class<?> clz) {
if (IS_HIDDEN_CLASS == null) {
return false;
}
try {
return (Boolean) IS_HIDDEN_CLASS.invoke(clz);
} catch (Exception e) {
throw new RuntimeException("Failed to determine whether class '" + clz.getName() + "' is hidden or not", e);
}
}
}

View File

@ -0,0 +1,75 @@
package com.comphenix.protocol.utility;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
/**
* This class can be used to reconstruct objects.
*
* Note that it is limited to classes where both the order and number of member variables matches the order and number
* of arguments for the first constructor. This means that this class is mostly useful for classes generated by lambdas.
*
* @param <T> The type of the object to reconstruct.
* @author Pim
*/
public class ObjectReconstructor<T> {
private final Class<T> clz;
private final Field[] fields;
private final Constructor<?> ctor;
public ObjectReconstructor(final Class<T> clz) {
this.clz = clz;
this.fields = clz.getDeclaredFields();
for (Field field : fields)
field.setAccessible(true);
this.ctor = clz.getDeclaredConstructors()[0];
this.ctor.setAccessible(true);
}
/**
* Gets the values of all member variables of the provided instance.
* @param instance The instance for which to get all the member variables.
* @return The values of the member variables from the instance.
*/
public Object[] getValues(final Object instance) {
final Object[] values = new Object[fields.length];
for (int idx = 0; idx < fields.length; ++idx)
try {
values[idx] = fields[idx].get(instance);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to access field: " + fields[idx].getName() +
" for class: " + clz.getName(), e);
}
return values;
}
/**
* Gets the fields in the class.
* @return The fields.
*/
public Field[] getFields() {
return fields;
}
/**
* Creates a new instance of the class using the new values.
* @param values The new values for the member variables of the class.
* @return The new instance.
*/
public T reconstruct(final Object[] values) {
if (values.length != fields.length)
throw new RuntimeException("Mismatched number of arguments for class: " + clz.getName());
try {
return (T) ctor.newInstance(values);
} catch (InstantiationException e) {
throw new RuntimeException("Failed to reconstruct object of type: " + clz.getName(), e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to access constructor of type: " + clz.getName(), e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Failed to invoke constructor of type: " + clz.getName(), e);
}
}
}