Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Java 15 #1025

Merged
merged 9 commits into from
Dec 23, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
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;
Expand All @@ -48,15 +50,16 @@
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;
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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -295,39 +313,39 @@ public ChannelPipeline pipeline() {

@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;
};
}

@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);

Expand All @@ -338,9 +356,9 @@ PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
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);
}
}

Expand All @@ -350,11 +368,12 @@ PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {

// 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);
}
});

Expand All @@ -363,6 +382,30 @@ PacketEvent handleScheduled(Object instance, FieldAccessor accessor) {
}
}

/**
* 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.
Expand Down Expand Up @@ -943,4 +986,15 @@ ChannelInjector getChannelInjector() {
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}