/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.debugger.jdi;
import com.intellij.debugger.SourcePosition;
import com.intellij.debugger.engine.ContextUtil;
import com.intellij.debugger.engine.DebugProcess;
import com.intellij.debugger.engine.StackFrameContext;
import com.intellij.debugger.engine.evaluation.EvaluateException;
import com.intellij.debugger.impl.DebuggerUtilsEx;
import com.intellij.debugger.impl.SimpleStackFrameContext;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.containers.MultiMap;
import com.sun.jdi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.org.objectweb.asm.MethodVisitor;
import org.jetbrains.org.objectweb.asm.Opcodes;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
/**
* From JDI sources:
*
validateStackFrame();
validateMirrors(variables);
int count = variables.size();
JDWP.StackFrame.GetValues.SlotInfo[] slots =
new JDWP.StackFrame.GetValues.SlotInfo[count];
for (int i=0; i<count; ++i) {
LocalVariableImpl variable = (LocalVariableImpl)variables.get(i);
if (!variable.isVisible(this)) {
throw new IllegalArgumentException(variable.name() +
" is not valid at this frame location");
}
slots[i] = new JDWP.StackFrame.GetValues.SlotInfo(variable.slot(),
(byte)variable.signature().charAt(0));
}
PacketStream ps;
synchronized (vm.state()) {
validateStackFrame();
ps = JDWP.StackFrame.GetValues.enqueueCommand(vm, thread, id, slots);
}
ValueImpl[] values;
try {
values = JDWP.StackFrame.GetValues.waitForReply(vm, ps).values;
} catch (JDWPException exc) {
switch (exc.errorCode()) {
case JDWP.Error.INVALID_FRAMEID:
case JDWP.Error.THREAD_NOT_SUSPENDED:
case JDWP.Error.INVALID_THREAD:
throw new InvalidStackFrameException();
default:
throw exc.toJDIException();
}
}
if (count != values.length) {
throw new InternalException(
"Wrong number of values returned from target VM");
}
Map map = new HashMap(count);
for (int i=0; i<count; ++i) {
LocalVariableImpl variable = (LocalVariableImpl)variables.get(i);
map.put(variable, values[i]);
}
return map;
*/
public class LocalVariablesUtil {
private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.jdi.LocalVariablesUtil");
private static final boolean ourInitializationOk;
private static Class<?> ourSlotInfoClass;
private static Constructor<?> slotInfoConstructor;
private static Method ourEnqueueMethod;
private static Method ourWaitForReplyMethod;
private static final boolean ourInitializationOkSet;
private static Class<?> ourSlotInfoClassSet;
private static Constructor<?> slotInfoConstructorSet;
private static Method ourEnqueueMethodSet;
private static Method ourWaitForReplyMethodSet;
static {
// get values init
boolean success = false;
try {
String GetValuesClassName = "com.sun.tools.jdi.JDWP$StackFrame$GetValues";
ourSlotInfoClass = Class.forName(GetValuesClassName + "$SlotInfo");
slotInfoConstructor = ourSlotInfoClass.getDeclaredConstructor(int.class, byte.class);
slotInfoConstructor.setAccessible(true);
Class<?> ourGetValuesClass = Class.forName(GetValuesClassName);
ourEnqueueMethod = getDeclaredMethodByName(ourGetValuesClass, "enqueueCommand");
ourWaitForReplyMethod = getDeclaredMethodByName(ourGetValuesClass, "waitForReply");
success = true;
}
catch (Throwable e) {
LOG.info(e);
}
ourInitializationOk = success;
// set value init
success = false;
try {
String setValuesClassName = "com.sun.tools.jdi.JDWP$StackFrame$SetValues";
ourSlotInfoClassSet = Class.forName(setValuesClassName + "$SlotInfo");
slotInfoConstructorSet = ourSlotInfoClassSet.getDeclaredConstructors()[0];
slotInfoConstructorSet.setAccessible(true);
Class<?> ourGetValuesClassSet = Class.forName(setValuesClassName);
ourEnqueueMethodSet = getDeclaredMethodByName(ourGetValuesClassSet, "enqueueCommand");
ourWaitForReplyMethodSet = getDeclaredMethodByName(ourGetValuesClassSet, "waitForReply");
success = true;
}
catch (Throwable e) {
LOG.info(e);
}
ourInitializationOkSet = success;
}
public static Map<DecompiledLocalVariable, Value> fetchValues(@NotNull StackFrameProxyImpl frameProxy,
DebugProcess process,
boolean full) throws Exception {
Map<DecompiledLocalVariable, Value> map = new LinkedHashMap<>(); // LinkedHashMap for correct order
Location location = frameProxy.location();
com.sun.jdi.Method method = location.method();
final int firstLocalVariableSlot = getFirstLocalsSlot(method);
// gather code variables names
MultiMap<Integer, String> namesMap =
full ? calcNames(new SimpleStackFrameContext(frameProxy, process), firstLocalVariableSlot) : MultiMap.empty();
// first add arguments
int slot = getFirstArgsSlot(method);
List<String> typeNames = method.argumentTypeNames();
List<Value> argValues = frameProxy.getArgumentValues();
for (int i = 0; i < argValues.size(); i++) {
map.put(new DecompiledLocalVariable(slot, true, null, namesMap.get(slot)), argValues.get(i));
slot += getTypeSlotSize(typeNames.get(i));
}
if (!full || !ourInitializationOk) {
return map;
}
// now try to fetch stack values
List<DecompiledLocalVariable> vars = collectVariablesFromBytecode(frameProxy.getVirtualMachine(), location, namesMap);
StackFrame frame = frameProxy.getStackFrame();
int size = vars.size();
while (size > 0) {
try {
return fetchSlotValues(map, vars.subList(0, size), frame);
}
catch (Exception e) {
LOG.debug(e);
}
size--; // try with the reduced list
}
return map;
}
private static Map<DecompiledLocalVariable, Value> fetchSlotValues(Map<DecompiledLocalVariable, Value> map,
List<DecompiledLocalVariable> vars,
StackFrame frame) throws Exception {
final Long frameId = ReflectionUtil.getField(frame.getClass(), frame, long.class, "id");
final VirtualMachine vm = frame.virtualMachine();
final Method stateMethod = vm.getClass().getDeclaredMethod("state");
stateMethod.setAccessible(true);
Object slotInfoArray = createSlotInfoArray(vars);
Object ps;
final Object vmState = stateMethod.invoke(vm);
synchronized(vmState) {
ps = ourEnqueueMethod.invoke(null, vm, frame.thread(), frameId, slotInfoArray);
}
final Object reply = ourWaitForReplyMethod.invoke(null, vm, ps);
final Value[] values = ReflectionUtil.getField(reply.getClass(), reply, Value[].class, "values");
if (vars.size() != values.length) {
throw new InternalException("Wrong number of values returned from target VM");
}
int idx = 0;
for (DecompiledLocalVariable var : vars) {
map.put(var, values[idx++]);
}
return map;
}
public static boolean canSetValues() {
return ourInitializationOkSet;
}
public static void setValue(StackFrame frame, int slot, Value value) throws EvaluateException {
try {
final Long frameId = ReflectionUtil.getField(frame.getClass(), frame, long.class, "id");
final VirtualMachine vm = frame.virtualMachine();
final Method stateMethod = vm.getClass().getDeclaredMethod("state");
stateMethod.setAccessible(true);
Object slotInfoArray = createSlotInfoArraySet(slot, value);
Object ps;
final Object vmState = stateMethod.invoke(vm);
synchronized (vmState) {
ps = ourEnqueueMethodSet.invoke(null, vm, frame.thread(), frameId, slotInfoArray);
}
ourWaitForReplyMethodSet.invoke(null, vm, ps);
}
catch (Exception e) {
throw new EvaluateException("Unable to set value", e);
}
}
private static Object createSlotInfoArraySet(int slot, Value value)
throws IllegalAccessException, InvocationTargetException, InstantiationException {
Object arrayInstance = Array.newInstance(ourSlotInfoClassSet, 1);
Array.set(arrayInstance, 0, slotInfoConstructorSet.newInstance(slot, value));
return arrayInstance;
}
private static Object createSlotInfoArray(Collection<DecompiledLocalVariable> vars) throws Exception {
final Object arrayInstance = Array.newInstance(ourSlotInfoClass, vars.size());
int idx = 0;
for (DecompiledLocalVariable var : vars) {
final Object info = slotInfoConstructor.newInstance(var.getSlot(), (byte)var.getSignature().charAt(0));
Array.set(arrayInstance, idx++, info);
}
return arrayInstance;
}
private static Method getDeclaredMethodByName(Class aClass, String methodName) throws NoSuchMethodException {
for (Method method : aClass.getDeclaredMethods()) {
if (methodName.equals(method.getName())) {
method.setAccessible(true);
return method;
}
}
throw new NoSuchMethodException(aClass.getName() + "." + methodName);
}
@NotNull
private static List<DecompiledLocalVariable> collectVariablesFromBytecode(VirtualMachineProxyImpl vm,
Location location,
MultiMap<Integer, String> namesMap) {
if (!vm.canGetBytecodes()) {
return Collections.emptyList();
}
try {
LOG.assertTrue(location != null);
final com.sun.jdi.Method method = location.method();
final Location methodLocation = method.location();
if (methodLocation == null || methodLocation.codeIndex() < 0) {
// native or abstract method
return Collections.emptyList();
}
long codeIndex = location.codeIndex();
if (codeIndex > 0) {
final byte[] bytecodes = method.bytecodes();
if (bytecodes != null && bytecodes.length > 0) {
final int firstLocalVariableSlot = getFirstLocalsSlot(method);
final HashMap<Integer, DecompiledLocalVariable> usedVars = new HashMap<>();
MethodBytecodeUtil.visit(method, codeIndex,
new MethodVisitor(Opcodes.API_VERSION) {
@Override
public void visitVarInsn(int opcode, int slot) {
if (slot >= firstLocalVariableSlot) {
DecompiledLocalVariable variable = usedVars.get(slot);
String typeSignature = MethodBytecodeUtil.getVarInstructionType(opcode).getDescriptor();
if (variable == null || !typeSignature.equals(variable.getSignature())) {
variable = new DecompiledLocalVariable(slot, false, typeSignature, namesMap.get(slot));
usedVars.put(slot, variable);
}
}
}
}, false);
if (usedVars.isEmpty()) {
return Collections.emptyList();
}
List<DecompiledLocalVariable> vars = new ArrayList<>(usedVars.values());
vars.sort(Comparator.comparingInt(DecompiledLocalVariable::getSlot));
return vars;
}
}
}
catch (UnsupportedOperationException ignored) {
}
catch (Exception e) {
LOG.error(e);
}
return Collections.emptyList();
}
@NotNull
private static MultiMap<Integer, String> calcNames(@NotNull final StackFrameContext context, final int firstLocalsSlot) {
SourcePosition position = ContextUtil.getSourcePosition(context);
if (position != null) {
return ReadAction.compute(() -> {
PsiElement element = position.getElementAt();
PsiElement method = DebuggerUtilsEx.getContainingMethod(element);
if (method != null) {
MultiMap<Integer, String> res = new MultiMap<>();
int slot = Math.max(0, firstLocalsSlot - getParametersStackSize(method));
for (PsiParameter parameter : DebuggerUtilsEx.getParameters(method)) {
res.putValue(slot, parameter.getName());
slot += getTypeSlotSize(parameter.getType());
}
PsiElement body = DebuggerUtilsEx.getBody(method);
if (body != null) {
try {
body.accept(new LocalVariableNameFinder(firstLocalsSlot, res, element));
}
catch (Exception e) {
LOG.info(e);
}
}
return res;
}
return MultiMap.empty();
});
}
return MultiMap.empty();
}
/**
* Walker that preserves the order of locals declarations but walks only visible scope
*/
private static class LocalVariableNameFinder extends JavaRecursiveElementVisitor {
private final MultiMap<Integer, String> myNames;
private int myCurrentSlotIndex;
private final PsiElement myElement;
private final Deque<Integer> myIndexStack = new LinkedList<>();
private boolean myReached = false;
public LocalVariableNameFinder(int startSlot, MultiMap<Integer, String> names, PsiElement element) {
myNames = names;
myCurrentSlotIndex = startSlot;
myElement = element;
}
private boolean shouldVisit(PsiElement scope) {
return !myReached && PsiTreeUtil.isContextAncestor(scope, myElement, false);
}
@Override
public void visitElement(PsiElement element) {
if (element == myElement) {
myReached = true;
}
else {
super.visitElement(element);
}
}
@Override
public void visitLocalVariable(PsiLocalVariable variable) {
super.visitLocalVariable(variable);
if (!myReached) {
appendName(variable.getName());
myCurrentSlotIndex += getTypeSlotSize(variable.getType());
}
}
public void visitSynchronizedStatement(PsiSynchronizedStatement statement) {
if (shouldVisit(statement)) {
myIndexStack.push(myCurrentSlotIndex);
try {
appendName("<monitor>");
myCurrentSlotIndex++;
super.visitSynchronizedStatement(statement);
}
finally {
myCurrentSlotIndex = myIndexStack.pop();
}
}
}
private void appendName(String varName) {
myNames.putValue(myCurrentSlotIndex, varName);
}
@Override
public void visitCodeBlock(PsiCodeBlock block) {
if (shouldVisit(block)) {
myIndexStack.push(myCurrentSlotIndex);
try {
super.visitCodeBlock(block);
}
finally {
myCurrentSlotIndex = myIndexStack.pop();
}
}
}
@Override
public void visitForStatement(PsiForStatement statement) {
if (shouldVisit(statement)) {
myIndexStack.push(myCurrentSlotIndex);
try {
super.visitForStatement(statement);
}
finally {
myCurrentSlotIndex = myIndexStack.pop();
}
}
}
@Override
public void visitForeachStatement(PsiForeachStatement statement) {
if (shouldVisit(statement)) {
myIndexStack.push(myCurrentSlotIndex);
try {
super.visitForeachStatement(statement);
}
finally {
myCurrentSlotIndex = myIndexStack.pop();
}
}
}
@Override
public void visitCatchSection(PsiCatchSection section) {
if (shouldVisit(section)) {
myIndexStack.push(myCurrentSlotIndex);
try {
super.visitCatchSection(section);
}
finally {
myCurrentSlotIndex = myIndexStack.pop();
}
}
}
@Override
public void visitResourceList(PsiResourceList resourceList) {
if (shouldVisit(resourceList)) {
myIndexStack.push(myCurrentSlotIndex);
try {
super.visitResourceList(resourceList);
}
finally {
myCurrentSlotIndex = myIndexStack.pop();
}
}
}
@Override
public void visitClass(PsiClass aClass) {
// skip local and anonymous classes
}
}
private static int getParametersStackSize(PsiElement method) {
return Arrays.stream(DebuggerUtilsEx.getParameters(method)).mapToInt(parameter -> getTypeSlotSize(parameter.getType())).sum();
}
private static int getTypeSlotSize(PsiType varType) {
if (PsiType.DOUBLE.equals(varType) || PsiType.LONG.equals(varType)) {
return 2;
}
return 1;
}
private static int getFirstArgsSlot(com.sun.jdi.Method method) {
return method.isStatic() ? 0 : 1;
}
private static int getFirstLocalsSlot(com.sun.jdi.Method method) {
return getFirstArgsSlot(method) + method.argumentTypeNames().stream().mapToInt(LocalVariablesUtil::getTypeSlotSize).sum();
}
private static int getTypeSlotSize(String name) {
if (PsiKeyword.DOUBLE.equals(name) || PsiKeyword.LONG.equals(name)) {
return 2;
}
return 1;
}
}