mirror of
https://github.com/dmulloy2/ProtocolLib.git
synced 2024-11-23 19:16:14 +01:00
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:
parent
7bac4ec634
commit
bbb053aa4e
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user