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