blob: 5a7d973e526dc877d72434548e5ee0be08698b97 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.debugger.jpda.backend.truffle;
import com.oracle.truffle.api.debug.Breakpoint;
import com.oracle.truffle.api.debug.DebugScope;
import com.oracle.truffle.api.debug.DebugStackFrame;
import com.oracle.truffle.api.debug.DebugValue;
import com.oracle.truffle.api.debug.Debugger;
import com.oracle.truffle.api.debug.DebuggerSession;
import com.oracle.truffle.api.debug.SuspendedEvent;
import com.oracle.truffle.api.nodes.LanguageInfo;
import com.oracle.truffle.api.source.SourceSection;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.graalvm.polyglot.Engine;
/**
* Truffle accessor for JPDA debugger.
*
* This class serves as a intermediary between the {@link JPDATruffleDebugManager}
* and JPDA Java debugger (<code>org.netbeans.modules.debugger.jpda.truffle.access.TruffleAccessor</code>),
* which submits breakpoints and calls methods of this class.
* To be able to invoke methods via debugger at any time, use {@link AccessLoop}.
*
* Creation of a PolyglotEngine instance is out of control for the debugger.
* Thus to intercept execution and suspension, we add Java method breakpoints into
* <code>com.oracle.truffle.api.vm.PolyglotEngine.dispatchExecutionEvent()</code> and
* <code>com.oracle.truffle.api.vm.PolyglotEngine.dispatchSuspendedEvent()</code> methods.
*
* @author Martin
*/
public class JPDATruffleAccessor extends Object {
static final boolean TRACE = Boolean.getBoolean("truffle.nbdebug.trace"); // NOI18N
private static final String ACCESS_THREAD_NAME = "JPDA Truffle Access Loop"; // NOI18N
private static volatile boolean accessLoopRunning = false;
private static volatile Thread accessLoopThread;
private static final Map<Debugger, JPDATruffleDebugManager> debugManagers = new WeakHashMap<>();
/** Explicitly set this field to true to step into script calls. */
static boolean isSteppingInto = false; // Step into was issued in JPDA debugger
static int steppingIntoTruffle = 0; // = 0 no stepping change, > 0 set step into, < 0 unset stepping into
/** A field to test for whether the access loop is sleeping and can be interrupted. */
static boolean accessLoopSleeping = false;
private static boolean stepIntoPrepared;
/** A cache of thread-local variables which prevents them from GC. */
private static final ThreadLocal<Set<Object>> threadVariablesCache = new ThreadLocal<Set<Object>>() {
@Override
protected Set<Object> initialValue() {
return new HashSet<>();
}
};
/** A step command:
* 0 no step (continue)
* 1 step into
* 2 step over
* 3 step out
*/
//private static int stepCmd = 0;
public JPDATruffleAccessor() {
// JDI needs to know about String class in this class loader.
new String("Initialize String class");
}
static Thread startAccessLoop() {
if (!accessLoopRunning) {
Thread loop;
AccessLoop accessLoop;
try {
accessLoop = new AccessLoop();
loop = new Thread(accessLoop, ACCESS_THREAD_NAME);
loop.setDaemon(true);
loop.setPriority(Thread.MIN_PRIORITY);
} catch (SecurityException se) {
return null;
}
accessLoopThread = loop;
accessLoopRunning = true;
loop.start();
}
return accessLoopThread;
}
static void stopAccessLoop() {
synchronized (debugManagers) {
for (JPDATruffleDebugManager debugManager : debugManagers.values()) {
debugManager.dispose();
}
}
accessLoopRunning = false;
if (accessLoopThread != null) {
accessLoopThread.interrupt();
accessLoopThread = null;
}
}
static JPDATruffleDebugManager setUpDebugManagerFor(/*Engine*/Object engineObj, boolean includeInternal, boolean doStepInto) {
trace("setUpDebugManagerFor("+engineObj+", "+doStepInto+")");
Engine engine = (Engine) engineObj;
Debugger debugger;
try {
debugger = engine.getInstruments().get("debugger").lookup(Debugger.class);
} catch (NullPointerException npe) {
// An engine without instruments/debugger. E.g. Engine.EMPTY
return null;
}
synchronized (debugManagers) {
if (debugManagers.containsKey(debugger)) {
return null;
}
}
JPDATruffleDebugManager tdm = new JPDATruffleDebugManager(debugger, includeInternal, doStepInto);
synchronized (debugManagers) {
debugManagers.put(debugger, tdm);
}
return tdm;
}
static void setIncludeInternal(boolean includeInternal) {
synchronized (debugManagers) {
for (JPDATruffleDebugManager tdm : debugManagers.values()) {
DebuggerSession debuggerSession = tdm.getDebuggerSession();
if (debuggerSession != null) {
debuggerSession.setSteppingFilter(JPDATruffleDebugManager.createSteppingFilter(includeInternal));
}
}
}
}
static int executionHalted(JPDATruffleDebugManager tdm,
SourcePosition position,
boolean haltedBefore,
DebugValue returnValue,
FrameInfo frameInfo,
boolean supportsJavaFrames,
Breakpoint[] breakpointsHit,
Throwable[] breakpointConditionExceptions,
int stepCmd) {
// Called when the execution is halted. Have a breakpoint here.
Set<Object> initialVars = threadVariablesCache.get();
assert initialVars != null;
// Clear again after execution is resumed.
threadVariablesCache.remove();
return stepCmd;
}
static void setStep(JPDATruffleDebugManager debugManager, int stepCmd) {
SuspendedEvent evt = debugManager.getCurrentSuspendedEvent();
switch (stepCmd) {
case 0: evt.prepareContinue();
break;
case 1: evt.prepareStepInto(1);
break;
case 2: evt.prepareStepOver(1);
break;
case 3: evt.prepareStepOut(1);
break;
default:
throw new IllegalStateException("Unknown step command: "+stepCmd);
}
}
static void suspendNextExecution() {
DebuggerSession[] sessions;
synchronized (debugManagers) {
sessions = new DebuggerSession[debugManagers.size()];
int i = 0;
for (JPDATruffleDebugManager tdm : debugManagers.values()) {
sessions[i++] = tdm.getDebuggerSession();
}
}
for (DebuggerSession session : sessions) {
session.suspendNextExecution();
}
}
/**
* Tries to suspend immediately at the current location of the current execution thread.
* When called from a suspension in a Java code, it reveals the guest code execution.
*
* @return the halted info, the array corresponds to the arguments of
* {@link #executionHalted(JPDATruffleDebugManager, SourcePosition, boolean, DebugValue, FrameInfo, boolean, Breakpoint[], Throwable[], int)},
* or <code>null</code>, when the suspend wasn't successful.
*/
static Object[] suspendHere() {
synchronized (debugManagers) {
for (JPDATruffleDebugManager tdm : debugManagers.values()) {
Object[] haltedInfo = tdm.suspendHere();
if (haltedInfo != null) {
return haltedInfo;
}
}
}
return null;
}
/**
* @param frames The array of stack frame infos
* @return An array of two elements: a String of frame information and
* an array of code contents.
*/
static Object[] getFramesInfo(DebugStackFrame[] frames, boolean includeInternal, boolean supportsJavaFrames) {
trace("getFramesInfo({0})",includeInternal);
int n = frames.length;
StringBuilder frameInfos = new StringBuilder();
String[] codes = new String[n];
Object[] thiss = new Object[n];
int j = 0;
for (int i = 0; i < n; i++) {
DebugStackFrame sf = frames[i];
boolean isInternal = FrameInfo.isInternal(sf);
//System.err.println("SF("+sf.getName()+", "+sf.getSourceSection()+") is internal = "+isInternal);
if (!includeInternal && isInternal) {
continue;
}
boolean isHost = supportsJavaFrames && FrameInfo.isHost(sf);
String sfName = sf.getName();
if (sfName == null) {
sfName = "";
}
frameInfos.append(sfName);
frameInfos.append('\n');
frameInfos.append(isHost);
frameInfos.append('\n');
LanguageInfo sfLang = sf.getLanguage();
String sfLangId = (sfLang != null) ? sfLang.getId() + " " + sfLang.getName() : "";
frameInfos.append(sfLangId);
frameInfos.append('\n');
frameInfos.append(DebuggerVisualizer.getSourceLocation(sf, isHost));
frameInfos.append('\n');
/*if (fi.getCallNode() == null) {
/* frames with null call nodes are filtered out by JPDATruffleDebugManager.FrameInfo
System.err.println("Frame with null call node: "+fi);
System.err.println(" is virtual frame = "+fi.isVirtualFrame());
System.err.println(" call target = "+fi.getCallTarget());
System.err.println("frameInfos = "+frameInfos);
*//*
}*/
SourcePosition position;
if (isHost) {
StackTraceElement ste = FrameInfo.getHostTraceElement(sf);
position = new SourcePosition(ste);
} else {
position = new SourcePosition(sf.getSourceSection(), sf.getLanguage());
}
frameInfos.append(createPositionIdentificationString(position));
if (includeInternal) {
frameInfos.append('\n');
frameInfos.append(isInternal);
}
frameInfos.append("\n\t\n");
codes[j] = position.code;
j++;
}
if (j < n) {
codes = Arrays.copyOf(codes, j);
thiss = Arrays.copyOf(thiss, j);
}
boolean areSkippedInternalFrames = j < n;
Object[] info = new Object[] { frameInfos.toString(), codes, thiss, areSkippedInternalFrames };
Set<Object> varCache = threadVariablesCache.get();
varCache.add(info);
return info;
}
private static String createPositionIdentificationString(SourcePosition position) {
StringBuilder str = new StringBuilder();
str.append(position.id);
str.append('\n');
str.append(position.name);
str.append('\n');
str.append(position.path);
str.append('\n');
str.append(position.hostClassName);
str.append('\n');
str.append(position.hostMethodName);
str.append('\n');
str.append(Objects.toString(position.uri));
str.append('\n');
str.append(position.mimeType);
str.append('\n');
str.append(position.sourceSection);
return str.toString();
}
static Object[] getTruffleAST(int depth) {
TruffleAST ast = TruffleAST.get(depth);
return new Object[] { ast.getNodes(), ast.getRawArguments(), ast.getRawSlots() };
}
// Unwind the current thread to given depth
static boolean setUnwind(int depth) {
SuspendedEvent evt = getCurrentSuspendedEvent();
if (evt == null) {
return false;
}
Iterator<DebugStackFrame> iterator = evt.getStackFrames().iterator();
DebugStackFrame frame = iterator.next();
while (depth > 0 && iterator.hasNext()) {
frame = iterator.next();
depth--;
}
if (depth != 0) {
return false;
}
evt.prepareUnwindFrame(frame);
return true;
}
// An array of scopes and their variables:
// <scope name>, <has receiver>, <num vars (including receiver, if any)>, [receiver + variables]
// See addValueElement() for the variable format
static Object[] getVariables(DebugStackFrame sf) {
List<Object> elements = new ArrayList<>();
try {
DebugScope receiverScope = null;
for (DebugScope scope = sf.getScope(); scope != null; scope = scope.getParent()) {
DebugValue receiver = scope.getReceiver();
boolean hasReceiver = receiver != null;
List<DebugValue> variables = new ArrayList<>();
if (hasReceiver) {
variables.add(receiver);
if (receiverScope == null) {
receiverScope = scope;
}
}
Iterable<DebugValue> varsIt = scope.getDeclaredValues();
Iterator<DebugValue> vars = varsIt.iterator();
if (!vars.hasNext()) {
continue;
}
while (vars.hasNext()) {
variables.add(vars.next());
}
if (variables.isEmpty()) {
continue;
}
elements.add(scope.getName());
elements.add(hasReceiver);
elements.add(variables.size());
for (DebugValue v : variables) {
addValueElement(v, elements);
}
}
if (elements.isEmpty() && receiverScope != null) {
// No variables, provide the receiver, at least:
elements.add(receiverScope.getName());
elements.add(true);
elements.add(1);
addValueElement(receiverScope.getReceiver(), elements);
}
} catch (ThreadDeath td) {
throw td;
} catch (Throwable t) {
LangErrors.exception("An error when accessing scopes", t);
}
Object[] variables = elements.toArray();
Set<Object> varCache = threadVariablesCache.get();
varCache.add(variables);
return variables;
}
// Store 12 elements: <name>, <language>, <type>, <readable>, <writable>, <internal>, <String value>,
// <var source>, <VS code>, <type source>, <TS code>, <DebugValue>
static void addValueElement(DebugValue value, List<Object> elements) {
GuestObject tobj = new GuestObject(value);
elements.add(tobj.name);
elements.add(tobj.language);
elements.add(tobj.type);
elements.add(tobj.readable);
elements.add(tobj.writable);
elements.add(tobj.internal);
elements.add(tobj.displayValue);
if (tobj.valueSourcePosition != null) {
elements.add(createPositionIdentificationString(tobj.valueSourcePosition));
elements.add(tobj.valueSourcePosition.code);
} else {
elements.add(null);
elements.add(null);
}
if (tobj.typeSourcePosition != null) {
elements.add(createPositionIdentificationString(tobj.typeSourcePosition));
elements.add(tobj.typeSourcePosition.code);
} else {
elements.add(null);
elements.add(null);
}
elements.add(tobj);
}
static void debuggerAccess() {
// A breakpoint is submitted on this method.
// When accessLoopThread is interrupted, this breakpoint is hit
// and methods can be executed via JPDA debugger.
}
static void breakpointResolvedAccess(Breakpoint breakpoint, int startLine, int startColumn) {
// A Java breakpoint is submitted on this method.
// When a Truffle breakpoint gets resolved, this method is called.
}
static Breakpoint[] setLineBreakpoint(String uriStr, int line,
int ignoreCount, String condition) throws URISyntaxException {
return doSetLineBreakpoint(new URI(uriStr), line, ignoreCount, condition, false);
}
static Breakpoint setLineBreakpoint(JPDATruffleDebugManager debugManager, String uriStr, int line,
int ignoreCount, String condition) throws URISyntaxException {
try {
return doSetLineBreakpoint(debugManager.getDebuggerSession(), new URI(uriStr), line, ignoreCount, condition, false);
} catch (IOException ex) {
System.err.println("setLineBreakpoint("+uriStr+", "+line+"): "+ex);
return null;
}
}
static Breakpoint[] setOneShotLineBreakpoint(String uriStr, int line) throws URISyntaxException {
return doSetLineBreakpoint(new URI(uriStr), line, 0, null, true);
}
private static Breakpoint[] doSetLineBreakpoint(URI uri, int line,
int ignoreCount, String condition,
boolean oneShot) {
Breakpoint[] lbs;
JPDATruffleDebugManager[] managers;
synchronized (debugManagers) {
managers = debugManagers.values().toArray(new JPDATruffleDebugManager[] {});
}
lbs = new Breakpoint[managers.length];
int i = 0;
for (JPDATruffleDebugManager debugManager : managers) {
DebuggerSession debuggerSession = debugManager.getDebuggerSession();
if (debuggerSession == null) {
lbs = Arrays.copyOf(lbs, lbs.length - 1);
//synchronized (debugManagers) {
// debugManagers.remove(debugger);
//}
continue;
}
Breakpoint lb;
try {
lb = doSetLineBreakpoint(debuggerSession, uri, line,
ignoreCount, condition, oneShot);
} catch (IOException dex) {
System.err.println("setLineBreakpoint("+uri+", "+line+"): "+dex);
lbs = Arrays.copyOf(lbs, lbs.length - 1);
continue;
}
lbs[i++] = lb;
}
return lbs;
}
private static Breakpoint doSetLineBreakpoint(DebuggerSession debuggerSession,
URI uri, int line,
int ignoreCount, String condition,
boolean oneShot) throws IOException {
Breakpoint.Builder bb = Breakpoint.newBuilder(uri).lineIs(line);
if (ignoreCount != 0) {
bb.ignoreCount(ignoreCount);
}
AtomicBoolean canNotifyResolved = new AtomicBoolean(false);
if (oneShot) {
bb.oneShot();
} else {
bb.resolveListener(new Breakpoint.ResolveListener() {
@Override
public void breakpointResolved(Breakpoint breakpoint, SourceSection section) {
trace("JPDATruffleAccessor breakpointResolved({0}, {1})", breakpoint, section);
// Notify breakpoint resolution after we actually install it.
// Resolution that is performed synchronously with the breakpoint installation
// would block doSetLineBreakpoint() method invocation on breakpointResolvedAccess breakpoint
if (canNotifyResolved.get()) {
breakpointResolvedAccess(breakpoint, section.getStartLine(), section.getStartColumn());
}
}
});
}
Breakpoint lb = bb.build();
if (condition != null) {
lb.setCondition(condition);
}
trace("JPDATruffleAccessor.setLineBreakpoint({0}, {1}, {2}): lb = {3}", debuggerSession, uri, line, lb);
Breakpoint breakpoint = debuggerSession.install(lb);
// We might return a resolved breakpoint already, or notify breakpointResolvedAccess later on
canNotifyResolved.set(true);
return breakpoint;
}
static void removeBreakpoint(Object br) {
((Breakpoint) br).dispose();
}
static Object evaluate(DebugStackFrame sf, String expression) {
DebugValue value = sf.eval(expression);
Object result = new GuestObject(value);
threadVariablesCache.get().add(result);
return result;
}
/** Get the suspended event on current thread, if any. */
private static SuspendedEvent getCurrentSuspendedEvent() {
synchronized (debugManagers) {
for (JPDATruffleDebugManager tdm : debugManagers.values()) {
SuspendedEvent evt = tdm.getCurrentSuspendedEvent();
if (evt != null) {
return evt;
}
}
}
return null;
}
static void trace(String message, Object... parameters) {
if (TRACE) {
System.out.println("NB Debugger: " + MessageFormat.format(message, parameters));
}
}
private static class AccessLoop implements Runnable {
@Override
public void run() {
while (accessLoopRunning) {
accessLoopSleeping = true;
// Wait until we're interrupted
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException iex) {}
accessLoopSleeping = false;
trace("AccessLoop: steppingIntoTruffle = "+steppingIntoTruffle+", isSteppingInto = "+isSteppingInto+", stepIntoPrepared = "+stepIntoPrepared);
if (steppingIntoTruffle != 0) {
if (steppingIntoTruffle > 0) {
if (!stepIntoPrepared) {
synchronized (debugManagers) {
for (JPDATruffleDebugManager debugManager : debugManagers.values()) {
debugManager.prepareExecStepInto();
}
}
stepIntoPrepared = true;
trace("Prepared step into and continue.");
}
isSteppingInto = true;
} else {
// un-prepare step into, if possible.
synchronized (debugManagers) {
for (JPDATruffleDebugManager debugManager : debugManagers.values()) {
debugManager.prepareExecContinue();
}
}
isSteppingInto = false;
stepIntoPrepared = false;
}
steppingIntoTruffle = 0;
continue;
}
trace("accessLoopRunning = "+accessLoopRunning+", possible debugger access...");
if (accessLoopRunning) {
debuggerAccess();
}
}
}
}
}