blob: c19fab844e168754f52c3bb9b83ccd1f7357ed58 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019, 2020 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
* IBM Corporation - Bug 448473 - [1.8][debug] Cannot use lambda expressions in breakpoint properties and display/expression view
*******************************************************************************/
package org.eclipse.jdt.internal.debug.eval;
import static org.eclipse.jdt.core.eval.ICodeSnippetRequestor.LOCAL_VAR_PREFIX;
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.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;
private String enclosingTypeName;
/**
* 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 classFiles
* @param codeSnippetClassName
* @param variableNames
* @param enclosingTypeName
*/
public RemoteEvaluator(LinkedHashMap<String, byte[]> classFiles, String codeSnippetClassName, List<String> variableNames, String enclosingTypeName) {
this.classFiles = classFiles;
this.enclosingTypeName = enclosingTypeName;
this.codeSnippetClassName = codeSnippetClassName.replace('.', '/');
this.variableNames = variableNames;
}
private IJavaClassObject loadTheClasses(IJavaThread theThread) throws DebugException {
if (loadedClass != null) {
return loadedClass;
}
JDIDebugTarget debugTarget = ((JDIDebugTarget) theThread.getDebugTarget());
IJavaClassObject theMainClass = null;
IJavaObject classloader = null;
IJavaReferenceType surroundingClass = findType(this.enclosingTypeName, debugTarget);
classloader = surroundingClass.getClassLoaderObject();
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 {
IJavaArray byteArray = createClassBytes(theThread, debugTarget, entry);
IJavaValue[] defineClassArgs = new IJavaValue[] { // args for defineClass
debugTarget.newValue(className.replaceAll("/", ".")), // class name //$NON-NLS-1$ //$NON-NLS-2$
byteArray, // classBytes,
debugTarget.newValue(0), // offset
debugTarget.newValue(entry.getValue().length), // length
debugTarget.nullValue() // protection domain
};
IJavaClassObject theClass = (IJavaClassObject) classloader.sendMessage("defineClass", "(Ljava/lang/String;[BIILjava/security/ProtectionDomain;)Ljava/lang/Class;", defineClassArgs, theThread, false); //$NON-NLS-1$//$NON-NLS-2$
if (codeSnippetClassName.equals(className)) {
theMainClass = theClass;
}
}
}
return theMainClass;
}
private IJavaArray createClassBytes(IJavaThread theThread, JDIDebugTarget debugTarget, Map.Entry<String, byte[]> entry) throws DebugException {
IJavaReferenceType byteArrayType = findType("byte[]", debugTarget);//$NON-NLS-1$
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);
return byteArray;
}
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.RemoteEvaluationEngine_Evaluation_failed___unable_to_find_injected_class,
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 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);
}
}