blob: ed3360115bc83d718c818ee05b0d3f8a344b507f [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.visual;
import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.ClassObjectReference;
import com.sun.jdi.ClassType;
import com.sun.jdi.Field;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.InvocationException;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.PrimitiveValue;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StringReference;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.debugger.Breakpoint;
import org.netbeans.api.debugger.DebuggerEngine;
import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.debugger.DebuggerManagerAdapter;
import org.netbeans.api.debugger.LazyDebuggerManagerListener;
import org.netbeans.api.debugger.Properties;
import org.netbeans.api.debugger.jpda.JPDADebugger;
import org.netbeans.api.debugger.jpda.JPDAThread;
import org.netbeans.api.debugger.jpda.MethodBreakpoint;
import org.netbeans.api.debugger.jpda.event.JPDABreakpointEvent;
import org.netbeans.api.debugger.jpda.event.JPDABreakpointListener;
import org.netbeans.api.io.InputOutput;
import org.netbeans.modules.debugger.jpda.expr.InvocationExceptionTranslated;
import org.netbeans.modules.debugger.jpda.expr.JDIVariable;
import org.netbeans.modules.debugger.jpda.jdi.ClassNotPreparedExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ClassObjectReferenceWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ClassTypeWrapper;
import org.netbeans.modules.debugger.jpda.jdi.InternalExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ObjectCollectedExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ObjectReferenceWrapper;
import org.netbeans.modules.debugger.jpda.jdi.PrimitiveValueWrapper;
import org.netbeans.modules.debugger.jpda.jdi.ReferenceTypeWrapper;
import org.netbeans.modules.debugger.jpda.jdi.UnsupportedOperationExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.VMDisconnectedExceptionWrapper;
import org.netbeans.modules.debugger.jpda.jdi.VirtualMachineWrapper;
import org.netbeans.modules.debugger.jpda.models.JPDAThreadImpl;
import org.netbeans.modules.debugger.jpda.visual.JavaComponentInfo.Stack;
import org.netbeans.modules.debugger.jpda.visual.breakpoints.AWTComponentBreakpointImpl;
import org.netbeans.modules.debugger.jpda.visual.options.Options;
import org.netbeans.modules.debugger.jpda.visual.ui.ScreenshotComponent;
import org.netbeans.spi.debugger.DebuggerServiceRegistration;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
/**
*
* @author Martin Entlicher
*/
@DebuggerServiceRegistration(path="", types=LazyDebuggerManagerListener.class)
public class VisualDebuggerListener extends DebuggerManagerAdapter {
private static final Logger logger = Logger.getLogger(VisualDebuggerListener.class.getName());
private static final Map<JPDADebugger, Map<ObjectReference, Stack>> componentsAndStackTraces
= new WeakHashMap<JPDADebugger, Map<ObjectReference, Stack>>();
private final Map<DebuggerEngine, Collection<Breakpoint>> helperComponentBreakpointsMap = new HashMap<DebuggerEngine, Collection<Breakpoint>>();
private final Properties properties;
private volatile Boolean isTrackComponentChanges = null;
private enum RemoteServiceInit {
SUCCESS,
FAIL,
FAIL_RETRY
}
public VisualDebuggerListener() {
final RequestProcessor rp = new RequestProcessor(VisualDebuggerListener.class);
properties = Options.getProperties();
if (RemoteAWTScreenshot.FAST_SNAPSHOT_RETRIEVAL) {
properties.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
String pName = evt.getPropertyName();
if (Options.PROPERTY_TCC.equals(pName)) {
final Object newValue = evt.getNewValue();
if (isTrackComponentChanges != null && !isTrackComponentChanges.equals(newValue)) {
rp.post(new Runnable() {
@Override
public void run() {
RemoteServices.attachHierarchyListeners(
Boolean.TRUE.equals(newValue),
RemoteServices.ServiceType.AWT);
}
});
isTrackComponentChanges = Boolean.TRUE.equals(newValue);
}
}
}
});
}
}
@Override
public void engineAdded(DebuggerEngine engine) {
// Create a BP in AWT and when hit, inject the remote service.
final JPDADebugger debugger = engine.lookupFirst(null, JPDADebugger.class);
if (debugger == null) {
return ;
}
boolean uploadAgent = Options.isUploadAgent();
logger.log(Level.FINE, "engineAdded({0}), debugger = {1}, uploadAgent = {2}", new Object[]{engine, debugger, uploadAgent});
Collection<Breakpoint> helperComponentBreakpoints = new ArrayList<Breakpoint>();
if (debugger != null && uploadAgent) {
final AtomicBoolean[] inited = new AtomicBoolean[] { new AtomicBoolean(false), new AtomicBoolean(false) };
final MethodBreakpoint[] mb = new MethodBreakpoint[2];
mb[0] = MethodBreakpoint.create("java.awt.EventQueue", "getNextEvent");
mb[1] = MethodBreakpoint.create("com.sun.javafx.tk.quantum.QuantumToolkit", "pulse");
mb[0].setBreakpointType(MethodBreakpoint.TYPE_METHOD_ENTRY);
mb[0].setSuspend(MethodBreakpoint.SUSPEND_EVENT_THREAD);
mb[0].setHidden(true);
mb[0].addJPDABreakpointListener(new JPDABreakpointListener() {
@Override
public void breakpointReached(JPDABreakpointEvent event) {
if (debugger.equals(event.getDebugger())) {
DebuggerManager.getDebuggerManager().removeBreakpoint(mb[0]);
if (inited[0].compareAndSet(false, true)) {
RemoteServiceInit initRet =
initDebuggerRemoteService(event.getThread(),
RemoteServices.ServiceType.AWT);
if (initRet.equals(RemoteServiceInit.FAIL_RETRY)) {
DebuggerManager.getDebuggerManager().addBreakpoint(mb[0]);
inited[0].set(false);
}
}
}
event.resume();
}
});
mb[1].setBreakpointType(MethodBreakpoint.TYPE_METHOD_ENTRY);
mb[1].setSuspend(MethodBreakpoint.SUSPEND_EVENT_THREAD);
mb[1].setHidden(true);
mb[1].addJPDABreakpointListener(new JPDABreakpointListener() {
@Override
public void breakpointReached(JPDABreakpointEvent event) {
if (debugger.equals(event.getDebugger())) {
DebuggerManager.getDebuggerManager().removeBreakpoint(mb[1]);
if (inited[1].compareAndSet(false, true)) {
RemoteServiceInit initRet =
initDebuggerRemoteService(event.getThread(),
RemoteServices.ServiceType.FX);
if (initRet.equals(RemoteServiceInit.FAIL_RETRY)) {
DebuggerManager.getDebuggerManager().addBreakpoint(mb[1]);
inited[1].set(false);
}
}
}
event.resume();
}
});
DebuggerManager.getDebuggerManager().addBreakpoint(mb[0]);
DebuggerManager.getDebuggerManager().addBreakpoint(mb[1]);
helperComponentBreakpoints.add(mb[0]);
helperComponentBreakpoints.add(mb[1]);
}
if (debugger != null) {
boolean trackComponentChanges = Options.isTrackComponentChanges();
isTrackComponentChanges = trackComponentChanges;
if (trackComponentChanges) {
if (!RemoteAWTScreenshot.FAST_SNAPSHOT_RETRIEVAL) {
MethodBreakpoint cmb = MethodBreakpoint.create("java.awt.Component", "createHierarchyEvents");
cmb.setHidden(true);
cmb.addJPDABreakpointListener(new JPDABreakpointListener() {
@Override
public void breakpointReached(JPDABreakpointEvent event) {
componentParentChanged(debugger, event, RemoteServices.ServiceType.AWT);
event.resume();
}
});
DebuggerManager.getDebuggerManager().addBreakpoint(cmb);
helperComponentBreakpoints.add(cmb);
}
MethodBreakpoint mb = MethodBreakpoint.create("javafx.scene.Node", "setParent");
mb.setHidden(true);
mb.addJPDABreakpointListener(new JPDABreakpointListener() {
@Override
public void breakpointReached(JPDABreakpointEvent event) {
componentParentChanged(debugger, event, RemoteServices.ServiceType.FX);
event.resume();
}
});
DebuggerManager.getDebuggerManager().addBreakpoint(mb);
helperComponentBreakpoints.add(mb);
}
}
synchronized (helperComponentBreakpointsMap) {
helperComponentBreakpointsMap.put(engine, helperComponentBreakpoints);
}
}
private RemoteServiceInit initDebuggerRemoteService(JPDAThread thread, RemoteServices.ServiceType sType) {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "initDebuggerRemoteService({0})", thread);
}
JPDAThreadImpl t = (JPDAThreadImpl) thread;
Lock writeLock = t.accessLock.writeLock();
writeLock.lock();
try {
ClassObjectReference cor = null;
try {
cor = RemoteServices.uploadBasicClasses(t, sType);
} catch (PropertyVetoException pvex) {
Exceptions.printStackTrace(pvex);
} catch (InvalidTypeException ex) {
Exceptions.printStackTrace(ex);
} catch (ClassNotLoadedException ex) {
Exceptions.printStackTrace(ex);
} catch (IncompatibleThreadStateException ex) {
return RemoteServiceInit.FAIL_RETRY;
} catch (InvocationException ex) {
Exceptions.printStackTrace(ex);
final InvocationExceptionTranslated iextr = new InvocationExceptionTranslated(ex, t.getDebugger());
initException(iextr, t);
Exceptions.printStackTrace(iextr);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Uploaded class = {0}", cor);
}
if (cor == null) {
return RemoteServiceInit.FAIL;
}
ThreadReference tr = t.getThreadReference();
if (sType == RemoteServices.ServiceType.FX) {
setFxDebug(tr.virtualMachine(), tr);
}
ClassType serviceClass = (ClassType) ClassObjectReferenceWrapper.reflectedType(cor);//RemoteServices.getClass(vm, "org.netbeans.modules.debugger.jpda.visual.remote.RemoteService");
InvocationExceptionTranslated iextr = null;
Method startMethod = ClassTypeWrapper.concreteMethodByName(serviceClass, "startAccessLoop", "()Z");
try {
t.notifyMethodInvoking();
Value ret = ClassTypeWrapper.invokeMethod(serviceClass, tr, startMethod, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
if (ret instanceof PrimitiveValue) {
boolean success = PrimitiveValueWrapper.booleanValue((PrimitiveValue) ret);
RemoteServices.setAccessLoopStarted(t.getDebugger(), success);
if (!success) {
return RemoteServiceInit.FAIL;
}
}
boolean trackComponentChanges = Options.isTrackComponentChanges();
isTrackComponentChanges = trackComponentChanges;
if (trackComponentChanges && RemoteAWTScreenshot.FAST_SNAPSHOT_RETRIEVAL) {
Method startHierarchyListenerMethod = ClassTypeWrapper.concreteMethodByName(serviceClass, "startHierarchyListener", "()Ljava/lang/String;");
if (startHierarchyListenerMethod != null) {
Value res = ClassTypeWrapper.invokeMethod(serviceClass, tr, startHierarchyListenerMethod, Collections.emptyList(), ObjectReference.INVOKE_SINGLE_THREADED);
if (res instanceof StringReference) {
String reason = ((StringReference) res).value();
InputOutput io = t.getDebugger().getConsoleIO().getIO();
if (io != null) {
io.getErr().println(NbBundle.getMessage(VisualDebuggerListener.class, "MSG_NoTrackingOfComponentChanges", reason));
}
//System.err.println("isHierarchyListenerAdded = false, reason = "+reason);
} else {
//System.err.println("isHierarchyListenerAdded = "+true);
}
}
}
} catch (VMDisconnectedExceptionWrapper vmd) {
} catch (InvocationException iex) {
iextr = new InvocationExceptionTranslated(iex, t.getDebugger());
Exceptions.printStackTrace(iex);
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
} finally {
t.notifyMethodInvokeDone();
ObjectReferenceWrapper.enableCollection(cor); // While AWTAccessLoop is running, it should not be collected.
}
if (iextr != null) {
initException(iextr, t);
Exceptions.printStackTrace(iextr);
}
} catch (InternalExceptionWrapper iex) {
} catch (ClassNotPreparedExceptionWrapper cnpex) {
Exceptions.printStackTrace(cnpex);
} catch (ObjectCollectedExceptionWrapper collex) {
} catch (UnsupportedOperationExceptionWrapper uex) {
logger.log(Level.INFO, uex.getLocalizedMessage(), uex);
} catch (VMDisconnectedExceptionWrapper vmd) {
} finally {
writeLock.unlock();
}
if (logger.isLoggable(Level.FINE)) {
try {
logger.fine("The RemoteServiceClass is there: "+
RemoteServices.getClass(t.getThreadReference().virtualMachine(),
"org.netbeans.modules.debugger.jpda.visual.remote.RemoteService"));
} catch (Exception ex) {
logger.log(Level.FINE, "", ex);
}
}
return RemoteServiceInit.SUCCESS;
}
private void initException(InvocationExceptionTranslated iextr, JPDAThreadImpl t) {
iextr.setPreferredThread(t);
iextr.getMessage();
iextr.getLocalizedMessage();
Throwable cause = iextr.getCause();
iextr.getStackTrace();
if (cause instanceof InvocationExceptionTranslated) {
initException((InvocationExceptionTranslated) cause, t);
}
}
@Override
public void engineRemoved(DebuggerEngine engine) {
ScreenshotComponent.closeScreenshots(engine);
JPDADebugger debugger = engine.lookupFirst(null, JPDADebugger.class);
if (debugger == null) {
return ;
}
logger.log(Level.FINE, "engineRemoved({0}), debugger = {1}", new Object[]{engine, debugger});
stopDebuggerRemoteService(debugger);
Collection<Breakpoint> helperComponentBreakpoints;
synchronized (helperComponentBreakpointsMap) {
helperComponentBreakpoints = helperComponentBreakpointsMap.remove(engine);
}
if (helperComponentBreakpoints != null && !helperComponentBreakpoints.isEmpty()) {
Iterator<Breakpoint> it = helperComponentBreakpoints.iterator();
while (it.hasNext()) {
DebuggerManager.getDebuggerManager().removeBreakpoint(it.next());
}
helperComponentBreakpoints.clear();
}
synchronized (componentsAndStackTraces) {
componentsAndStackTraces.remove(debugger);
}
}
private void stopDebuggerRemoteService(JPDADebugger d) {
ClassObjectReference serviceClass = RemoteServices.getServiceClass(d, RemoteServices.ServiceType.AWT);
if (serviceClass == null) {
return ;
}
try {
ReferenceType serviceType = serviceClass.reflectedType();
Field awtAccessLoop = serviceType.fieldByName("awtAccessLoop"); // NOI18N
if (awtAccessLoop != null) {
((ClassType) serviceType).setValue(awtAccessLoop, serviceClass.virtualMachine().mirrorOf(false));
}
serviceClass.enableCollection();
} catch (VMDisconnectedException vdex) {
// Ignore
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
}
public static Stack getStackOf(JPDADebugger debugger, ObjectReference component) {
synchronized(componentsAndStackTraces) {
Map<ObjectReference, Stack> cs = componentsAndStackTraces.get(debugger);
if (cs != null) {
return cs.get(component);
} else {
return null;
}
}
}
private static void componentParentChanged(JPDADebugger debugger, JPDABreakpointEvent event,
RemoteServices.ServiceType serviceType) {
ObjectReference component = null;
ObjectReference[] parentPtr = null;
try {
if (RemoteServices.ServiceType.AWT.equals(serviceType)) {
parentPtr = new ObjectReference[] { null };
component = AWTComponentBreakpointImpl.getComponentOfParentChanged(event, parentPtr);
} else {
JPDAThread t = event.getThread();
JDIVariable v = (JDIVariable)t.getCallStack(0, 1)[0].getThisVariable();
component = (ObjectReference)v.getJDIValue();
}
} catch (Exception ex) {}
if (component != null) {
if (parentPtr != null && parentPtr[0] == null) {
// Component was removed
synchronized (componentsAndStackTraces) {
Map<ObjectReference, Stack> componentAndStackTrace = componentsAndStackTraces.get(debugger);
if (componentAndStackTrace != null) {
componentAndStackTrace.remove(component);
}
}
return ;
}
Stack stack;
try {
stack = new Stack(event.getThread().getCallStack());
} catch (AbsentInformationException ex) {
return;
}
//System.err.println("Have following stack for component "+component+", all = "+all+": "+stack);
synchronized (componentsAndStackTraces) {
Map<ObjectReference, Stack> componentAndStackTrace = componentsAndStackTraces.get(debugger);
if (componentAndStackTrace == null) {
componentAndStackTrace = new HashMap<ObjectReference, Stack>();
componentsAndStackTraces.put(debugger, componentAndStackTrace);
}
//System.err.println("Component "+component+" has changed parent from "+Arrays.asList(stack.getFrames()));
//System.err.println(" Parent = "+((JDIVariable) event.getVariable()).getJDIValue());
componentAndStackTrace.put(component, stack);
}
}
}
/**
* JavaFX runtime is boobietrapped with various checks for {@linkplain com.sun.javafx.runtime.SystemProperties#isDebug() }
* which lead to spurious NPEs. Need to make it happy and force the runtime into debug mode
*/
private static void setFxDebug(VirtualMachine vm, ThreadReference tr) {
ClassType sysPropClass = getClass(vm, tr, "com.sun.javafx.runtime.SystemProperties");
if(sysPropClass == null) {
// openjfx doesn't have runtime.SystemProperties.isDebug
return;
}
try {
Field debugFld = ReferenceTypeWrapper.fieldByName(sysPropClass, "isDebug"); // NOI18N
sysPropClass.setValue(debugFld, VirtualMachineWrapper.mirrorOf(vm, true));
} catch (VMDisconnectedExceptionWrapper vmdex) {
} catch (InternalExceptionWrapper iex) {
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
}
private static ClassType getClass(VirtualMachine vm, ThreadReference tr, String name) {
ReferenceType t = getType(vm, tr, name);
if (t instanceof ClassType) {
return (ClassType)t;
}
// logger.log(Level.WARNING, "{0} is not a class but {1}", new Object[]{name, t}); // NOI18N
return null;
}
private static ReferenceType getType(VirtualMachine vm, ThreadReference tr, String name) {
try {
List<ReferenceType> classList = VirtualMachineWrapper.classesByName(vm, name);
if (!classList.isEmpty()) {
return classList.iterator().next();
}
List<ReferenceType> classClassList = VirtualMachineWrapper.classesByName(vm, "java.lang.Class"); // NOI18N
if (classClassList.isEmpty()) {
throw new IllegalStateException("Cannot load class Class"); // NOI18N
}
ClassType cls = (ClassType) classClassList.iterator().next();
Method m = ClassTypeWrapper.concreteMethodByName(cls, "forName", "(Ljava/lang/String;)Ljava/lang/Class;"); // NOI18N
StringReference mirrorOfName = VirtualMachineWrapper.mirrorOf(vm, name);
try {
cls.invokeMethod(tr, m, Collections.singletonList(mirrorOfName), ObjectReference.INVOKE_SINGLE_THREADED);
List<ReferenceType> classList2 = VirtualMachineWrapper.classesByName(vm, name);
if (!classList2.isEmpty()) {
return classList2.iterator().next();
}
} catch (InvalidTypeException ex) {
logger.log(Level.FINE, "Cannot load class " + name, ex); // NOI18N
} catch (ClassNotLoadedException ex) {
logger.log(Level.FINE, "Cannot load class " + name, ex); // NOI18N
} catch (IncompatibleThreadStateException ex) {
logger.log(Level.FINE, "Cannot load class " + name, ex); // NOI18N
} catch (InvocationException ex) {
logger.log(Level.FINE, "Cannot load class " + name, ex); // NOI18N
}
} catch (ClassNotPreparedExceptionWrapper ex) {
logger.log(Level.FINE, "Not prepared class ", ex); // NOI18N
} catch (UnsupportedOperationExceptionWrapper uoex) {
} catch (InternalExceptionWrapper iex) {
} catch (VMDisconnectedExceptionWrapper vmdex) {
}
return null;
}
}