package rocks.inspectit.shared.all.kryonet.rmi; import static com.esotericsoftware.minlog.Log.DEBUG; import static com.esotericsoftware.minlog.Log.TRACE; import static com.esotericsoftware.minlog.Log.WARN; import static com.esotericsoftware.minlog.Log.debug; import static com.esotericsoftware.minlog.Log.trace; import static com.esotericsoftware.minlog.Log.warn; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.PriorityQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.KryoSerializable; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import com.esotericsoftware.kryo.serializers.FieldSerializer; import com.esotericsoftware.kryo.util.IntMap; import com.esotericsoftware.kryonet.FrameworkMessage; import com.esotericsoftware.kryonet.rmi.RemoteObject; import com.esotericsoftware.kryonet.rmi.TimeoutException; import rocks.inspectit.shared.all.kryonet.Connection; import rocks.inspectit.shared.all.kryonet.EndPoint; import rocks.inspectit.shared.all.kryonet.Listener; /** * Allows methods on objects to be invoked remotely over TCP. Objects are * {@link #register(int, Object) registered} with an ID. The remote end of connections that have * been {@link #addConnection(Connection) added} are allowed to * {@link #getRemoteObject(Connection, int, Class) access} registered objects. * <p> * It costs at least 2 bytes more to use remote method invocation than just sending the parameters. * If the method has a return value which is not {@link RemoteObject#setNonBlocking(boolean) * ignored}, an extra byte is written. If the type of a parameter is not final (note primitives are * final) then an extra byte is written for that parameter. * <p> * <b>IMPORTANT:</b> The class code is copied/taken/based from * <a href="https://github.com/EsotericSoftware/kryonet">kryonet</a>. Original author is Nathan * Sweet. License info can be found * <a href="https://github.com/EsotericSoftware/kryonet/blob/master/license.txt">here</a>. * * @author Nathan Sweet <misc@n4te.com> */ @SuppressWarnings({ "all", "unchecked" }) // NOCHKALL public class ObjectSpace { static private final byte kReturnValMask = (byte) 0x80; // 1000 0000 static private final byte kReturnExMask = (byte) 0x40; // 0100 0000 static private final Object instancesLock = new Object(); static ObjectSpace[] instances = new ObjectSpace[0]; static private final HashMap<Class, CachedMethod[]> methodCache = new HashMap(); final IntMap idToObject = new IntMap(); Connection[] connections = {}; final Object connectionsLock = new Object(); Executor executor; private final Listener invokeListener = new Listener() { @Override public void received(final Connection connection, Object object) { if (!(object instanceof InvokeMethod)) { return; } if (connections != null) { int i = 0, n = connections.length; for (; i < n; i++) { if (connection == connections[i]) { break; } } if (i == n) { return; // The InvokeMethod message is not for a connection in this ObjectSpace. } } final InvokeMethod invokeMethod = (InvokeMethod) object; final Object target = idToObject.get(invokeMethod.objectID); if (target == null) { if (WARN) { warn("kryonet", "Ignoring remote invocation request for unknown object ID: " + invokeMethod.objectID); } return; } if (executor == null) { invoke(connection, target, invokeMethod); } else { executor.execute(new Runnable() { @Override public void run() { invoke(connection, target, invokeMethod); } }); } } @Override public void disconnected(Connection connection) { removeConnection(connection); } }; /** * Creates an ObjectSpace with no connections. Connections must be * {@link #addConnection(Connection) added} to allow the remote end of the connections to access * objects in this ObjectSpace. */ public ObjectSpace() { synchronized (instancesLock) { ObjectSpace[] instances = ObjectSpace.instances; ObjectSpace[] newInstances = new ObjectSpace[instances.length + 1]; newInstances[0] = this; System.arraycopy(instances, 0, newInstances, 1, instances.length); ObjectSpace.instances = newInstances; } } /** * Creates an ObjectSpace with the specified connection. More connections can be * {@link #addConnection(Connection) added}. */ public ObjectSpace(Connection connection) { this(); addConnection(connection); } /** * Sets the executor used to invoke methods when an invocation is received from a remote * endpoint. By default, no executor is set and invocations occur on the network thread, which * should not be blocked for long. * * @param executor * May be null. */ public void setExecutor(Executor executor) { this.executor = executor; } /** * Registers an object to allow the remote end of the ObjectSpace's connections to access it * using the specified ID. * <p> * If a connection is added to multiple ObjectSpaces, the same object ID should not be * registered in more than one of those ObjectSpaces. * * @see #getRemoteObject(Connection, int, Class...) */ public void register(int objectID, Object object) { if (object == null) { throw new IllegalArgumentException("object cannot be null."); } idToObject.put(objectID, object); if (TRACE) { trace("kryonet", "Object registered with ObjectSpace as " + objectID + ": " + object); } } /** * Removes an object. The remote end of the ObjectSpace's connections will no longer be able to * access it. */ public void remove(int objectID) { Object object = idToObject.remove(objectID); if (TRACE) { trace("kryonet", "Object " + objectID + " removed from ObjectSpace: " + object); } } /** * Removes an object. The remote end of the ObjectSpace's connections will no longer be able to * access it. */ public void remove(Object object) { if (!idToObject.containsValue(object, true)) { return; } int objectID = idToObject.findKey(object, true, -1); idToObject.remove(objectID); if (TRACE) { trace("kryonet", "Object " + objectID + " removed from ObjectSpace: " + object); } } /** * Causes this ObjectSpace to stop listening to the connections for method invocation messages. */ public void close() { Connection[] connections = this.connections; for (Connection connection : connections) { connection.removeListener(invokeListener); } synchronized (instancesLock) { ArrayList<Connection> temp = new ArrayList(Arrays.asList(instances)); temp.remove(this); instances = temp.toArray(new ObjectSpace[temp.size()]); } if (TRACE) { trace("kryonet", "Closed ObjectSpace."); } } /** * Allows the remote end of the specified connection to access objects registered in this * ObjectSpace. */ public void addConnection(Connection connection) { if (connection == null) { throw new IllegalArgumentException("connection cannot be null."); } synchronized (connectionsLock) { Connection[] newConnections = new Connection[connections.length + 1]; newConnections[0] = connection; System.arraycopy(connections, 0, newConnections, 1, connections.length); connections = newConnections; } connection.addListener(invokeListener); if (TRACE) { trace("kryonet", "Added connection to ObjectSpace: " + connection); } } /** * Removes the specified connection, it will no longer be able to access objects registered in * this ObjectSpace. */ public void removeConnection(Connection connection) { if (connection == null) { throw new IllegalArgumentException("connection cannot be null."); } connection.removeListener(invokeListener); synchronized (connectionsLock) { ArrayList<Connection> temp = new ArrayList(Arrays.asList(connections)); temp.remove(connection); connections = temp.toArray(new Connection[temp.size()]); } if (TRACE) { trace("kryonet", "Removed connection from ObjectSpace: " + connection); } } /** * Invokes the method on the object and, if necessary, sends the result back to the connection * that made the invocation request. This method is invoked on the update thread of the * {@link EndPoint} for this ObjectSpace and unless an {@link #setExecutor(Executor) executor} * has been set. * * @param connection * The remote side of this connection requested the invocation. */ protected void invoke(Connection connection, Object target, InvokeMethod invokeMethod) { if (DEBUG) { String argString = ""; if (invokeMethod.args != null) { argString = Arrays.deepToString(invokeMethod.args); argString = argString.substring(1, argString.length() - 1); } debug("kryonet", connection + " received: " + target.getClass().getSimpleName() + "#" + invokeMethod.method.getName() + "(" + argString + ")"); } byte responseID = invokeMethod.responseID; boolean transmitReturnVal = (responseID & kReturnValMask) == kReturnValMask; boolean transmitExceptions = (responseID & kReturnExMask) == kReturnExMask; Object result = null; Method method = invokeMethod.method; try { result = method.invoke(target, invokeMethod.args); // Catch exceptions caused by the Method#invoke } catch (InvocationTargetException ex) { if (transmitExceptions) { result = ex.getCause(); } else { throw new RuntimeException("Error invoking method: " + method.getDeclaringClass().getName() + "." + method.getName(), ex); } } catch (Exception ex) { throw new RuntimeException("Error invoking method: " + method.getDeclaringClass().getName() + "." + method.getName(), ex); } if (responseID == 0) { return; } InvokeMethodResult invokeMethodResult = new InvokeMethodResult(); invokeMethodResult.objectID = invokeMethod.objectID; invokeMethodResult.responseID = responseID; // Do not return non-primitives if transmitReturnVal is false if (!transmitReturnVal && !invokeMethod.method.getReturnType().isPrimitive()) { invokeMethodResult.result = null; } else { invokeMethodResult.result = result; } int length = connection.sendTCP(invokeMethodResult); if (DEBUG) { debug("kryonet", connection + " sent: " + result + " (" + length + ")"); } } /** * Identical to {@link #getRemoteObject(Connection, int, Class...)} except returns the object * cast to the specified interface type. The returned object still implements * {@link RemoteObject}. */ static public <T> T getRemoteObject(final Connection connection, int objectID, Class<T> iface) { return (T) getRemoteObject(connection, objectID, new Class[] { iface }); } /** * Returns a proxy object that implements the specified interfaces. Methods invoked on the proxy * object will be invoked remotely on the object with the specified ID in the ObjectSpace for * the specified connection. If the remote end of the connection has not * {@link #addConnection(Connection) added} the connection to the ObjectSpace, the remote method * invocations will be ignored. * <p> * Methods that return a value will throw {@link TimeoutException} if the response is not * received with the {@link RemoteObject#setResponseTimeout(int) response timeout}. * <p> * If {@link RemoteObject#setNonBlocking(boolean) non-blocking} is false (the default), then * methods that return a value must not be called from the update thread for the connection. An * exception will be thrown if this occurs. Methods with a void return value can be called on * the update thread. * <p> * If a proxy returned from this method is part of an object graph sent over the network, the * object graph on the receiving side will have the proxy object replaced with the registered * object. * * @see RemoteObject */ static public RemoteObject getRemoteObject(Connection connection, int objectID, Class... ifaces) { if (connection == null) { throw new IllegalArgumentException("connection cannot be null."); } if (ifaces == null) { throw new IllegalArgumentException("ifaces cannot be null."); } Class[] temp = new Class[ifaces.length + 1]; temp[0] = RemoteObject.class; System.arraycopy(ifaces, 0, temp, 1, ifaces.length); return (RemoteObject) Proxy.newProxyInstance(ObjectSpace.class.getClassLoader(), temp, new RemoteInvocationHandler(connection, objectID)); } /** Handles network communication when methods are invoked on a proxy. */ static private class RemoteInvocationHandler implements InvocationHandler { private final Connection connection; final int objectID; private int timeoutMillis = 3000; private boolean nonBlocking = false; private boolean transmitReturnValue = true; private boolean transmitExceptions = true; private Byte lastResponseID; private byte nextResponseNum = 1; private Listener responseListener; final ReentrantLock lock = new ReentrantLock(); final Condition responseCondition = lock.newCondition(); final ConcurrentHashMap<Byte, InvokeMethodResult> responseTable = new ConcurrentHashMap(); public RemoteInvocationHandler(Connection connection, final int objectID) { super(); this.connection = connection; this.objectID = objectID; responseListener = new Listener() { @Override public void received(Connection connection, Object object) { if (!(object instanceof InvokeMethodResult)) { return; } InvokeMethodResult invokeMethodResult = (InvokeMethodResult) object; if (invokeMethodResult.objectID != objectID) { return; } responseTable.put(invokeMethodResult.responseID, invokeMethodResult); lock.lock(); try { responseCondition.signalAll(); } finally { lock.unlock(); } } @Override public void disconnected(Connection connection) { close(); } }; connection.addListener(responseListener); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Exception { if (method.getDeclaringClass() == RemoteObject.class) { String name = method.getName(); if (name.equals("close")) { close(); return null; } else if (name.equals("setResponseTimeout")) { timeoutMillis = (Integer) args[0]; return null; } else if (name.equals("setNonBlocking")) { nonBlocking = (Boolean) args[0]; return null; } else if (name.equals("setTransmitReturnValue")) { transmitReturnValue = (Boolean) args[0]; return null; } else if (name.equals("setTransmitExceptions")) { transmitExceptions = (Boolean) args[0]; return null; } else if (name.equals("waitForLastResponse")) { if (lastResponseID == null) { throw new IllegalStateException("There is no last response to wait for."); } return waitForResponse(lastResponseID); } else if (name.equals("getLastResponseID")) { if (lastResponseID == null) { throw new IllegalStateException("There is no last response ID."); } return lastResponseID; } else if (name.equals("waitForResponse")) { if (!transmitReturnValue && !transmitExceptions && nonBlocking) { throw new IllegalStateException("This RemoteObject is currently set to ignore all responses."); } return waitForResponse((Byte) args[0]); } else if (name.equals("getConnection")) { return connection; } else { // Should never happen, for debugging purposes only throw new RuntimeException("Invocation handler could not find RemoteObject method. Check ObjectSpace.java"); } } else if (method.getDeclaringClass() == Object.class) { if (method.getName().equals("toString")) { return "<proxy>"; } try { return method.invoke(proxy, args); } catch (Exception ex) { throw new RuntimeException(ex); } } InvokeMethod invokeMethod = new InvokeMethod(); invokeMethod.objectID = objectID; invokeMethod.method = method; invokeMethod.args = args; // The only time a invocation doesn't need a response is if it's async // and no return values or exceptions are wanted back. boolean needsResponse = transmitReturnValue || transmitExceptions || !nonBlocking; if (needsResponse) { byte responseID; synchronized (this) { // Increment the response counter and put it into the first six bits of the // responseID byte responseID = nextResponseNum++; if (nextResponseNum == 64) { nextResponseNum = 1; // Keep number under 2^6, avoid 0 (see else statement // below) } } // Pack return value and exception info into the top two bits if (transmitReturnValue) { responseID |= kReturnValMask; } if (transmitExceptions) { responseID |= kReturnExMask; } invokeMethod.responseID = responseID; } else { invokeMethod.responseID = 0; // A response info of 0 means to not respond } int length = connection.sendTCP(invokeMethod); if (DEBUG) { String argString = ""; if (args != null) { argString = Arrays.deepToString(args); argString = argString.substring(1, argString.length() - 1); } debug("kryonet", connection + " sent: " + method.getDeclaringClass().getSimpleName() + "#" + method.getName() + "(" + argString + ") (" + length + ")"); } if (invokeMethod.responseID != 0) { lastResponseID = invokeMethod.responseID; } if (nonBlocking) { Class returnType = method.getReturnType(); if (returnType.isPrimitive()) { if (returnType == int.class) { return 0; } if (returnType == boolean.class) { return Boolean.FALSE; } if (returnType == float.class) { return 0f; } if (returnType == char.class) { return (char) 0; } if (returnType == long.class) { return 0l; } if (returnType == short.class) { return (short) 0; } if (returnType == byte.class) { return (byte) 0; } if (returnType == double.class) { return 0d; } } return null; } try { Object result = waitForResponse(invokeMethod.responseID); if ((result != null) && (result instanceof Exception)) { throw (Exception) result; } else { return result; } } catch (TimeoutException ex) { throw new TimeoutException("Response timed out: " + method.getDeclaringClass().getName() + "." + method.getName()); } } private Object waitForResponse(byte responseID) { if (connection.getEndPoint().getUpdateThread() == Thread.currentThread()) { throw new IllegalStateException("Cannot wait for an RMI response on the connection's update thread."); } long endTime = System.currentTimeMillis() + timeoutMillis; while (true) { long remaining = endTime - System.currentTimeMillis(); if (responseTable.containsKey(responseID)) { InvokeMethodResult invokeMethodResult = responseTable.get(responseID); responseTable.remove(responseID); lastResponseID = null; return invokeMethodResult.result; } else { if (remaining <= 0) { throw new TimeoutException("Response timed out."); } lock.lock(); try { responseCondition.await(remaining, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } finally { lock.unlock(); } } } } void close() { connection.removeListener(responseListener); } } /** Internal message to invoke methods remotely. */ static public class InvokeMethod implements FrameworkMessage, KryoSerializable { public int objectID; public Method method; public Object[] args; // The top two bytes of the ID indicate if the remote invocation should respond with return // values and exceptions, // respectively. The rest is a six bit counter. This means up to 63 responses can be stored // before undefined behavior // occurs due to possible duplicate IDs. public byte responseID; @Override public void write(Kryo kryo, Output output) { output.writeInt(objectID, true); int methodClassID = kryo.getRegistration(method.getDeclaringClass()).getId(); output.writeInt(methodClassID, true); CachedMethod[] cachedMethods = getMethods(kryo, method.getDeclaringClass()); CachedMethod cachedMethod = null; for (int i = 0, n = cachedMethods.length; i < n; i++) { cachedMethod = cachedMethods[i]; if (cachedMethod.method.equals(method)) { output.writeByte(i); break; } } for (int i = 0, n = cachedMethod.serializers.length; i < n; i++) { Serializer serializer = cachedMethod.serializers[i]; if (serializer != null) { kryo.writeObjectOrNull(output, args[i], serializer); } else { kryo.writeClassAndObject(output, args[i]); } } output.writeByte(responseID); } @Override public void read(Kryo kryo, Input input) { objectID = input.readInt(true); int methodClassID = input.readInt(true); Class methodClass = kryo.getRegistration(methodClassID).getType(); byte methodIndex = input.readByte(); CachedMethod cachedMethod; try { cachedMethod = getMethods(kryo, methodClass)[methodIndex]; } catch (IndexOutOfBoundsException ex) { throw new KryoException("Invalid method index " + methodIndex + " for class: " + methodClass.getName()); } method = cachedMethod.method; args = new Object[cachedMethod.serializers.length]; for (int i = 0, n = args.length; i < n; i++) { Serializer serializer = cachedMethod.serializers[i]; if (serializer != null) { args[i] = kryo.readObjectOrNull(input, method.getParameterTypes()[i], serializer); } else { args[i] = kryo.readClassAndObject(input); } } responseID = input.readByte(); } } /** Internal message to return the result of a remotely invoked method. */ static public class InvokeMethodResult implements FrameworkMessage { public int objectID; public byte responseID; public Object result; } static CachedMethod[] getMethods(Kryo kryo, Class type) { CachedMethod[] cachedMethods = methodCache.get(type); if (cachedMethods != null) { return cachedMethods; } ArrayList<Method> allMethods = new ArrayList(); Class nextClass = type; while ((nextClass != null) && (nextClass != Object.class)) { Collections.addAll(allMethods, nextClass.getDeclaredMethods()); nextClass = nextClass.getSuperclass(); } PriorityQueue<Method> methods = new PriorityQueue(Math.max(1, allMethods.size()), new Comparator<Method>() { @Override public int compare(Method o1, Method o2) { // Methods are sorted so they can be represented as an index. int diff = o1.getName().compareTo(o2.getName()); if (diff != 0) { return diff; } Class[] argTypes1 = o1.getParameterTypes(); Class[] argTypes2 = o2.getParameterTypes(); if (argTypes1.length > argTypes2.length) { return 1; } if (argTypes1.length < argTypes2.length) { return -1; } for (int i = 0; i < argTypes1.length; i++) { diff = argTypes1[i].getName().compareTo(argTypes2[i].getName()); if (diff != 0) { return diff; } } throw new RuntimeException("Two methods with same signature!"); // Impossible. } }); for (int i = 0, n = allMethods.size(); i < n; i++) { Method method = allMethods.get(i); int modifiers = method.getModifiers(); if (Modifier.isStatic(modifiers)) { continue; } if (Modifier.isPrivate(modifiers)) { continue; } if (method.isSynthetic()) { continue; } methods.add(method); } int n = methods.size(); cachedMethods = new CachedMethod[n]; for (int i = 0; i < n; i++) { CachedMethod cachedMethod = new CachedMethod(); cachedMethod.method = methods.poll(); // Store the serializer for each final parameter. Class[] parameterTypes = cachedMethod.method.getParameterTypes(); cachedMethod.serializers = new Serializer[parameterTypes.length]; for (int ii = 0, nn = parameterTypes.length; ii < nn; ii++) { if (kryo.isFinal(parameterTypes[ii])) { cachedMethod.serializers[ii] = kryo.getSerializer(parameterTypes[ii]); } } cachedMethods[i] = cachedMethod; } methodCache.put(type, cachedMethods); return cachedMethods; } /** * Returns the first object registered with the specified ID in any of the ObjectSpaces the * specified connection belongs to. */ static Object getRegisteredObject(Connection connection, int objectID) { ObjectSpace[] instances = ObjectSpace.instances; for (ObjectSpace objectSpace : instances) { // Check if the connection is in this ObjectSpace. Connection[] connections = objectSpace.connections; for (Connection connection2 : connections) { if (connection2 != connection) { continue; } // Find an object with the objectID. Object object = objectSpace.idToObject.get(objectID); if (object != null) { return object; } } } return null; } /** * Registers the classes needed to use ObjectSpaces. This should be called before any * connections are opened. * * @param nextRegistrationId * Registration id to start registering at. * * @return updated value of the next registration id * * @see Kryo#register(Class, Serializer) */ static public int registerClasses(final Kryo kryo, int nextRegistrationId) { kryo.register(Object[].class, nextRegistrationId++); kryo.register(InvokeMethod.class, nextRegistrationId++); FieldSerializer serializer = (FieldSerializer) kryo.register(InvokeMethodResult.class, nextRegistrationId++).getSerializer(); serializer.getField("objectID").setClass(int.class, new Serializer<Integer>() { @Override public void write(Kryo kryo, Output output, Integer object) { output.writeInt(object, true); } @Override public Integer read(Kryo kryo, Input input, Class<Integer> type) { return input.readInt(true); } }); kryo.register(InvocationHandler.class, new Serializer() { @Override public void write(Kryo kryo, Output output, Object object) { RemoteInvocationHandler handler = (RemoteInvocationHandler) Proxy.getInvocationHandler(object); output.writeInt(handler.objectID, true); } @Override public Object read(Kryo kryo, Input input, Class type) { int objectID = input.readInt(true); Connection connection = (Connection) kryo.getContext().get("connection"); Object object = getRegisteredObject(connection, objectID); if (WARN && (object == null)) { warn("kryonet", "Unknown object ID " + objectID + " for connection: " + connection); } return object; } }, nextRegistrationId++); return nextRegistrationId; } static class CachedMethod { Method method; Serializer[] serializers; } }