blob: 02224b9b72b7f3321594f5ac5ca5ba0ea1447a2b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019 Jesper Steen Møller and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Jesper Steen Møller - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.debug.eval;
import static org.eclipse.jdt.internal.eval.EvaluationConstants.LOCAL_VAR_PREFIX;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.core.eval.ICodeSnippetRequestor;
import org.eclipse.jdt.debug.core.IJavaArray;
import org.eclipse.jdt.debug.core.IJavaArrayType;
import org.eclipse.jdt.debug.core.IJavaClassObject;
import org.eclipse.jdt.debug.core.IJavaClassType;
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
import org.eclipse.jdt.debug.core.IJavaFieldVariable;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaType;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.core.IJavaVariable;
import org.eclipse.jdt.debug.core.JDIDebugModel;
import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
import org.eclipse.jdt.internal.debug.core.model.JDIDebugTarget;
import org.eclipse.jdt.internal.debug.core.model.JDIValue;
import com.sun.jdi.InvocationException;
import com.sun.jdi.ObjectReference;
/**
* An evaluation engine that deploys class files to a debuggee by using Unsafe through the JDWP.
*/
public class RemoteEvaluator {
private final LinkedHashMap<String, byte[]> classFiles;
private final String codeSnippetClassName;
private final List<String> variableNames;
private IJavaClassObject loadedClass = null;
/**
* Constructs a new evaluation engine for the given VM in the context of the specified project. Class files required for the evaluation will be
* deployed to the specified directory (which must be on the class path of the VM in order for evaluation to work).
*
* @param codeSnippetClassName
* @param classFiles2
* @param variableNames
*/
public RemoteEvaluator(LinkedHashMap<String, byte[]> classFiles, String codeSnippetClassName, List<String> variableNames) {
this.classFiles = classFiles;
this.codeSnippetClassName = codeSnippetClassName.replace('.', '/');
this.variableNames = variableNames;
}
private IJavaClassObject loadTheClasses(IJavaThread theThread) throws DebugException {
if (loadedClass != null) {
return loadedClass;
}
JDIDebugTarget debugTarget = ((JDIDebugTarget) theThread.getDebugTarget());
IJavaClassType unsafeClass = (IJavaClassType) findType("sun.misc.Unsafe", debugTarget); //$NON-NLS-1$
// IJavaValue[] getDeclaredFieldArgs = new IJavaValue[] { getDebugTarget().newValue("theUnsafe") }; //$NON-NLS-1$
IJavaFieldVariable theField = unsafeClass.getField("theUnsafe"); //$NON-NLS-1$
IJavaObject theUnsafe = (IJavaObject) theField.getValue();
// IJavaValue[] setAccessibleArgs = new IJavaValue[] { getDebugTarget().newValue(true) };
// theField.sendMessage(
// "setAccessible", "(Z)V", setAccessibleArgs, getThread(), false); //$NON-NLS-2$ //$NON-NLS-1$
// IJavaValue[] getArgs = new IJavaValue[] { getDebugTarget().newValue(null) };
// IJavaObject theUnsafe = (IJavaObject) theField
// .sendMessage(
// "get", "()Ljava/lang/Object;", getArgs, getThread(), false); //$NON-NLS-2$ //$NON-NLS-1$
IJavaClassObject theMainClass = null;
IJavaReferenceType byteArrayType = findType("byte[]", debugTarget);//$NON-NLS-1$
for (Map.Entry<String, byte[]> entry : classFiles.entrySet()) {
String className = entry.getKey();
IJavaReferenceType existingClass = tryLoadType(className, debugTarget);
if (existingClass != null) {
if (codeSnippetClassName.equals(className)) {
theMainClass = existingClass.getClassObject();
}
} else {
byte[] classBytes = entry.getValue();
IJavaArray byteArray = ((IJavaArrayType) byteArrayType).newInstance(classBytes.length);
IJavaValue[] debugClassBytes = new IJavaValue[classBytes.length];
for (int ix = 0; ix < classBytes.length; ++ix) {
debugClassBytes[ix] = ((JDIDebugTarget) theThread.getDebugTarget()).newValue(classBytes[ix]);
}
byteArray.setValues(debugClassBytes);
IJavaValue[] defineClassArgs = new IJavaValue[] {
debugTarget.newValue(className),
byteArray, // classBytes,
debugTarget.newValue(0), debugTarget.newValue(classBytes.length), debugTarget.nullValue(), // classloader
debugTarget.nullValue() // protection domain
};
IJavaClassObject theClass = (IJavaClassObject) theUnsafe.sendMessage("defineClass", "(Ljava/lang/String;[BIILjava/lang/ClassLoader;Ljava/security/ProtectionDomain;)Ljava/lang/Class;", defineClassArgs, theThread, false); //$NON-NLS-1$//$NON-NLS-2$
if (codeSnippetClassName.equals(className)) {
theMainClass = theClass;
}
}
}
return theMainClass;
}
private IJavaReferenceType findType(String typeName, IJavaDebugTarget debugTarget) throws DebugException {
IJavaReferenceType theClass = tryLoadType(typeName, debugTarget);
if (theClass == null) {
// unable to load the class
throw new DebugException(
new Status(
IStatus.ERROR,
JDIDebugModel.getPluginIdentifier(),
DebugException.REQUEST_FAILED,
EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_instantiate_code_snippet_class__11,
null));
}
return theClass;
}
private IJavaReferenceType tryLoadType(String typeName, IJavaDebugTarget debugTarget) throws DebugException {
IJavaReferenceType clazz = null;
IJavaType[] types = debugTarget.getJavaTypes(typeName);
if (types != null && types.length > 0) {
clazz = (IJavaReferenceType) types[0];
}
return clazz;
}
/**
* Initializes the value of instance variables in the 'code snippet object' that are used as place-holders for free variables and 'this' in the
* current stack frame.
*
* @param object
* instance of code snippet class that will be run
* @param boundValues
* popped values which should be injected into the code snippet object.
* @exception DebugException
* if an exception is thrown accessing the given object
*/
protected void initializeFreeVars(IJavaObject object, IJavaValue boundValues[]) throws DebugException {
if (boundValues.length != this.variableNames.size()) {
throw new DebugException(new Status(IStatus.ERROR, JDIDebugModel.getPluginIdentifier(), DebugException.REQUEST_FAILED, EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_initialize_local_variables__4, null));
}
for (int i = 0; i < boundValues.length; ++i) {
IJavaVariable field = object.getField(new String(LOCAL_VAR_PREFIX) + this.variableNames.get(i), false);
if (field != null) {
IJavaValue bound = boundValues[i];
field.setValue(bound);
} else {
// System.out.print(Arrays.asList(((IJavaReferenceType) object.getJavaType()).getAllFieldNames()));
throw new DebugException(new Status(IStatus.ERROR, JDIDebugModel.getPluginIdentifier(), DebugException.REQUEST_FAILED, EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_initialize_local_variables__4, null));
}
}
}
/**
* Constructs and returns a new instance of the specified class on the
* target VM.
*
* @param className
* fully qualified class name
* @return a new instance on the target, as an <code>IJavaValue</code>
* @exception DebugException
* if creation fails
*/
protected IJavaObject newInstance(IJavaThread theThread) throws DebugException {
IJavaDebugTarget debugTarget = ((IJavaDebugTarget) theThread.getDebugTarget());
IJavaObject object = null;
IJavaClassObject clazz = loadTheClasses(theThread);
if (clazz == null) {
// The class is not loaded on the target VM.
// Force the load of the class.
IJavaType[] types = debugTarget.getJavaTypes("java.lang.Class"); //$NON-NLS-1$
IJavaClassType classClass = null;
if (types != null && types.length > 0) {
classClass = (IJavaClassType) types[0];
}
if (classClass == null) {
// unable to load the class
throw new DebugException(
new Status(
IStatus.ERROR,
JDIDebugModel.getPluginIdentifier(),
DebugException.REQUEST_FAILED,
EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___unable_to_instantiate_code_snippet_class__11,
null));
}
IJavaValue[] args = new IJavaValue[] { debugTarget.newValue(
this.codeSnippetClassName) };
IJavaObject classObject = (IJavaObject) classClass
.sendMessage(
"forName", "(Ljava/lang/String;)Ljava/lang/Class;", args, theThread); //$NON-NLS-2$ //$NON-NLS-1$
object = (IJavaObject) classObject
.sendMessage(
"newInstance", "()Ljava/lang/Object;", null, theThread, false); //$NON-NLS-2$ //$NON-NLS-1$
} else {
object = (IJavaObject) clazz.sendMessage("newInstance", "()Ljava/lang/Object;", null, theThread, false); //$NON-NLS-2$ //$NON-NLS-1$
// object = clazz.newInstance("<init>", null, theThread); //$NON-NLS-1$
}
return object;
}
/**
* Interprets and returns the result of the running the snippet class file.
* The type of the result is described by an instance of
* <code>java.lang.Class</code>. The value is interpreted based on the
* result type.
* <p>
* Objects as well as primitive data types (boolean, int, etc.), have class
* objects, which are created by the VM. If the class object represents a
* primitive data type, then the associated value is stored in an instance
* of its "object" class. For example, when the result type is the class
* object for <code>int</code>, the result object is an instance of
* <code>java.lang.Integer</code>, and the actual <code>int</code> is stored
* in the </code>intValue()</code>. When the result type is the class object
* for <code>java.lang.Integer</code> the result object is an instance of
* <code>java.lang.Integer</code>, to be interpreted as a
* <code>java.lang.Integer</code>.
* </p>
*
* @param resultType
* the class of the result
* @param resultValue
* the value of the result, to be interpreted based on
* resultType
* @return the result of running the code snippet class file
*/
protected IJavaValue convertResult(IJavaDebugTarget debugTarget, IJavaClassObject resultType,
IJavaValue result) throws DebugException {
if (resultType == null) {
// there was an exception or compilation problem - no result
return null;
}
// check the type of the result - if a primitive type, convert it
String sig = resultType.getInstanceType().getSignature();
if (sig.equals("V") || sig.equals("Lvoid;")) { //$NON-NLS-2$ //$NON-NLS-1$
// void
return debugTarget.voidValue();
}
if (result.getJavaType() == null) {
// null result
return result;
}
if (sig.length() == 1) {
// primitive type - find the instance variable with the
// signature of the result type we are looking for
IVariable[] vars = result.getVariables();
IJavaVariable var = null;
for (IVariable var2 : vars) {
IJavaVariable jv = (IJavaVariable) var2;
if (!jv.isStatic() && jv.getSignature().equals(sig)) {
var = jv;
break;
}
}
if (var != null) {
return (IJavaValue) var.getValue();
}
} else {
// an object
return result;
}
throw new DebugException(
new Status(
IStatus.ERROR,
JDIDebugModel.getPluginIdentifier(),
DebugException.REQUEST_FAILED,
EvaluationMessages.LocalEvaluationEngine_Evaluation_failed___internal_error_retreiving_result__17,
null));
}
/**
* Returns a copy of the type name with '$' replaced by '.', or returns
* <code>null</code> if the given type name refers to an anonymous inner
* class.
*
* @param typeName
* a fully qualified type name
* @return a copy of the type name with '$' replaced by '.', or returns
* <code>null</code> if the given type name refers to an anonymous
* inner class.
*/
protected String getTranslatedTypeName(String typeName) {
int index = typeName.lastIndexOf('$');
if (index == -1) {
return typeName;
}
if (index + 1 > typeName.length()) {
// invalid name
return typeName;
}
String last = typeName.substring(index + 1);
try {
Integer.parseInt(last);
return null;
} catch (NumberFormatException e) {
return typeName.replace('$', '.');
}
}
/**
* Returns an array of simple type names that are part of the given type's
* qualified name. For example, if the given name is <code>x.y.A$B</code>,
* an array with <code>["A", "B"]</code> is returned.
*
* @param typeName
* fully qualified type name
* @return array of nested type names
*/
protected String[] getNestedTypeNames(String typeName) {
int index = typeName.lastIndexOf('.');
if (index >= 0) {
typeName = typeName.substring(index + 1);
}
index = typeName.indexOf('$');
ArrayList<String> list = new ArrayList<>(1);
while (index >= 0) {
list.add(typeName.substring(0, index));
typeName = typeName.substring(index + 1);
index = typeName.indexOf('$');
}
list.add(typeName);
return list.toArray(new String[list.size()]);
}
/**
/**
* Returns the name of the code snippet to instantiate to run the current
* evaluation.
*
* @return the name of the deployed code snippet to instantiate and run
*/
protected String getCodeSnippetClassName() {
return codeSnippetClassName;
}
public IJavaValue evaluate(IJavaThread theThread, IJavaValue[] args) throws DebugException {
IJavaObject codeSnippetInstance = null;
IJavaDebugTarget debugTarget = ((IJavaDebugTarget) theThread.getDebugTarget());
try {
codeSnippetInstance = newInstance(theThread);
initializeFreeVars(codeSnippetInstance, args);
codeSnippetInstance.sendMessage(ICodeSnippetRequestor.RUN_METHOD, "()V", null, theThread, false); //$NON-NLS-1$
// now retrieve the description of the result
IVariable[] fields = codeSnippetInstance.getVariables();
IJavaVariable resultValue = null;
IJavaVariable resultType = null;
for (IVariable field : fields) {
if (field.getName().equals(ICodeSnippetRequestor.RESULT_TYPE_FIELD)) {
resultType = (IJavaVariable) field;
}
if (field.getName().equals(ICodeSnippetRequestor.RESULT_VALUE_FIELD)) {
resultValue = (IJavaVariable) field;
}
}
IJavaValue result = convertResult(debugTarget, (IJavaClassObject) resultType.getValue(), (IJavaValue) resultValue.getValue());
return result;
} catch (DebugException e) {
Throwable underlyingException = e.getStatus().getException();
if (underlyingException instanceof InvocationException) {
ObjectReference theException = ((InvocationException) underlyingException).exception();
if (theException != null) {
try {
try {
IJavaObject v = (IJavaObject) JDIValue.createValue((JDIDebugTarget) debugTarget, theException);
v.sendMessage("printStackTrace", "()V", null, theThread, false); //$NON-NLS-2$ //$NON-NLS-1$
} catch (DebugException de) {
JDIDebugPlugin.log(de);
}
} catch (RuntimeException re) {
JDIDebugPlugin.log(re);
}
}
}
throw e;
}
}
public int getVariableCount() {
return this.variableNames.size();
}
public String getVariableName(int i) {
return this.variableNames.get(i);
}
}