/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.jdb.server; import com.sun.jdi.AbsentInformationException; import com.sun.jdi.Bootstrap; import com.sun.jdi.ClassNotPreparedException; import com.sun.jdi.IncompatibleThreadStateException; import com.sun.jdi.NativeMethodException; import com.sun.jdi.ReferenceType; import com.sun.jdi.ThreadReference; import com.sun.jdi.VMCannotBeModifiedException; import com.sun.jdi.VirtualMachine; import com.sun.jdi.connect.AttachingConnector; import com.sun.jdi.connect.Connector; import com.sun.jdi.connect.IllegalConnectorArgumentsException; import com.sun.jdi.request.BreakpointRequest; import com.sun.jdi.request.ClassPrepareRequest; import com.sun.jdi.request.EventRequest; import com.sun.jdi.request.EventRequestManager; import com.sun.jdi.request.InvalidRequestStateException; import com.sun.jdi.request.StepRequest; import org.eclipse.che.api.debug.shared.dto.BreakpointDto; import org.eclipse.che.api.debug.shared.dto.FieldDto; import org.eclipse.che.api.debug.shared.dto.LocationDto; import org.eclipse.che.api.debug.shared.dto.StackFrameDumpDto; import org.eclipse.che.api.debug.shared.dto.VariableDto; import org.eclipse.che.api.debug.shared.dto.VariablePathDto; import org.eclipse.che.api.debug.shared.dto.action.ResumeActionDto; import org.eclipse.che.api.debug.shared.model.Breakpoint; import org.eclipse.che.api.debug.shared.model.DebuggerInfo; import org.eclipse.che.api.debug.shared.model.Location; import org.eclipse.che.api.debug.shared.model.SimpleValue; import org.eclipse.che.api.debug.shared.model.Variable; import org.eclipse.che.api.debug.shared.model.VariablePath; import org.eclipse.che.api.debug.shared.model.action.ResumeAction; import org.eclipse.che.api.debug.shared.model.action.StartAction; import org.eclipse.che.api.debug.shared.model.action.StepIntoAction; import org.eclipse.che.api.debug.shared.model.action.StepOutAction; import org.eclipse.che.api.debug.shared.model.action.StepOverAction; import org.eclipse.che.api.debug.shared.model.impl.BreakpointImpl; import org.eclipse.che.api.debug.shared.model.impl.DebuggerInfoImpl; import org.eclipse.che.api.debug.shared.model.impl.FieldImpl; import org.eclipse.che.api.debug.shared.model.impl.LocationImpl; import org.eclipse.che.api.debug.shared.model.impl.SimpleValueImpl; import org.eclipse.che.api.debug.shared.model.impl.VariableImpl; import org.eclipse.che.api.debug.shared.model.impl.event.BreakpointActivatedEventImpl; import org.eclipse.che.api.debug.shared.model.impl.event.DisconnectEventImpl; import org.eclipse.che.api.debug.shared.model.impl.event.SuspendEventImpl; import org.eclipse.che.api.debugger.server.Debugger; import org.eclipse.che.api.debugger.server.exceptions.DebuggerException; import org.eclipse.che.plugin.jdb.server.exceptions.DebuggerAbsentInformationException; import org.eclipse.che.plugin.jdb.server.expression.Evaluator; import org.eclipse.che.plugin.jdb.server.expression.ExpressionException; import org.eclipse.che.plugin.jdb.server.expression.ExpressionParser; import org.eclipse.che.plugin.jdb.server.utils.JavaDebuggerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.eclipse.che.dto.server.DtoFactory.newDto; /** * Connects to JVM over Java Debug Wire Protocol handle its events. All methods of this class may throws * DebuggerException. Typically such exception caused by errors in underlying JDI (Java Debug Interface), e.g. * connection errors. Instance of Debugger is not thread-safe. * * @author andrew00x * @author Artem Zatsarynnyi * @author Valeriy Svydenko */ public class JavaDebugger implements EventsHandler, Debugger { private static final Logger LOG = LoggerFactory.getLogger(JavaDebugger.class); private static final JavaDebuggerUtils debuggerUtil = new JavaDebuggerUtils(); private final String host; private final int port; private final DebuggerCallback debuggerCallback; /** * A mapping of source file names to breakpoints. This mapping is used to set * breakpoints in files that haven't been loaded yet by a target Java VM. */ private final ConcurrentMap<String, List<Breakpoint>> deferredBreakpoints = new ConcurrentHashMap<>(); /** Stores ClassPrepareRequests to prevent making duplicate class prepare requests. */ private final ConcurrentMap<String, ClassPrepareRequest> classPrepareRequests = new ConcurrentHashMap<>(); /** Target Java VM representation. */ private VirtualMachine vm; private EventsCollector eventsCollector; /** Current thread. Not <code>null</code> is thread suspended, e.g breakpoint reached. */ private ThreadReference thread; /** Current stack frame. Not <code>null</code> is thread suspended, e.g breakpoint reached. */ private JdiStackFrame stackFrame; /** Lock for synchronization debug processes. */ private Lock lock = new ReentrantLock(); /** * Create debugger and connect it to the JVM which already running at the specified host and port. * * @param host * the host where JVM running * @param port * the Java Debug Wire Protocol (JDWP) port * @throws DebuggerException * when connection to Java VM is not established */ JavaDebugger(String host, int port, DebuggerCallback debuggerCallback) throws DebuggerException { this.host = host; this.port = port; this.debuggerCallback = debuggerCallback; connect(); } /** * Attach to a JVM that is already running at specified host. * * @throws DebuggerException * when connection to Java VM is not established */ private void connect() throws DebuggerException { final String connectorName = "com.sun.jdi.SocketAttach"; AttachingConnector connector = connector(connectorName); if (connector == null) { throw new DebuggerException( String.format("Unable connect to target Java VM. Requested connector '%s' not found. ", connectorName)); } Map<String, Connector.Argument> arguments = connector.defaultArguments(); arguments.get("hostname").setValue(host); ((Connector.IntegerArgument)arguments.get("port")).setValue(port); int attempt = 0; for (; ; ) { try { Thread.sleep(2000); vm = connector.attach(arguments); vm.suspend(); break; } catch (UnknownHostException | IllegalConnectorArgumentsException e) { throw new DebuggerException(e.getMessage(), e); } catch (IOException e) { LOG.error(e.getMessage(), e); if (++attempt > 10) { throw new DebuggerException(e.getMessage(), e); } try { Thread.sleep(2000); } catch (InterruptedException ignored) { } } catch (InterruptedException ignored) { } } eventsCollector = new EventsCollector(vm.eventQueue(), this); LOG.debug("Connect {}:{}", host, port); } private AttachingConnector connector(String connectorName) { for (AttachingConnector c : Bootstrap.virtualMachineManager().attachingConnectors()) { if (connectorName.equals(c.name())) { return c; } } return null; } @Override public DebuggerInfo getInfo() throws DebuggerException { return new DebuggerInfoImpl(host, port, vm.name(), vm.version(), 0, null); } @Override public void start(StartAction action) throws DebuggerException { for (Breakpoint b : action.getBreakpoints()) { try { addBreakpoint(b); } catch (DebuggerException e) { // can't add breakpoint, skip it } } vm.resume(); } @Override public void disconnect() throws DebuggerException { vm.dispose(); LOG.debug("Close connection to {}:{}", host, port); } @Override public void addBreakpoint(Breakpoint breakpoint) throws DebuggerException { final String className = findFQN(breakpoint); final int lineNumber = breakpoint.getLocation().getLineNumber(); List<ReferenceType> classes = vm.classesByName(className); // it may mean that class doesn't loaded by a target JVM yet if (classes.isEmpty()) { deferBreakpoint(breakpoint); throw new DebuggerException("Class not loaded"); } ReferenceType clazz = classes.get(0); List<com.sun.jdi.Location> locations; try { locations = clazz.locationsOfLine(lineNumber); } catch (AbsentInformationException | ClassNotPreparedException e) { throw new DebuggerException(e.getMessage(), e); } if (locations.isEmpty()) { throw new DebuggerException("Line " + lineNumber + " not found in class " + className); } com.sun.jdi.Location location = locations.get(0); if (location.method() == null) { // Line is out of method. throw new DebuggerException("Invalid line " + lineNumber + " in class " + className); } // Ignore new breakpoint if already have breakpoint at the same location. EventRequestManager requestManager = getEventManager(); for (BreakpointRequest breakpointRequest : requestManager.breakpointRequests()) { if (location.equals(breakpointRequest.location())) { LOG.debug("Breakpoint at {} already set", location); return; } } try { EventRequest breakPointRequest = requestManager.createBreakpointRequest(location); breakPointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL); String expression = breakpoint.getCondition(); if (!(expression == null || expression.isEmpty())) { ExpressionParser parser = ExpressionParser.newInstance(expression); breakPointRequest.putProperty("org.eclipse.che.ide.java.debug.condition.expression.parser", parser); } breakPointRequest.setEnabled(true); } catch (NativeMethodException | IllegalThreadStateException | InvalidRequestStateException e) { throw new DebuggerException(e.getMessage(), e); } debuggerCallback.onEvent( new BreakpointActivatedEventImpl( new BreakpointImpl(breakpoint.getLocation(), true, breakpoint.getCondition()))); LOG.debug("Add breakpoint: {}", location); } private String findFQN(Breakpoint breakpoint) throws DebuggerException { Location location = breakpoint.getLocation(); final String parentFqn = location.getTarget(); final String projectPath = location.getResourceProjectPath(); int lineNumber = location.getLineNumber(); return debuggerUtil.findFqnByPosition(projectPath, parentFqn, lineNumber); } private void deferBreakpoint(Breakpoint breakpoint) throws DebuggerException { final String className = breakpoint.getLocation().getTarget(); List<Breakpoint> newList = new ArrayList<>(); List<Breakpoint> list = deferredBreakpoints.putIfAbsent(className, newList); if (list == null) { list = newList; } list.add(breakpoint); // start listening for the load of the type if (!classPrepareRequests.containsKey(className)) { ClassPrepareRequest request = getEventManager().createClassPrepareRequest(); // set class filter in order to reduce the amount of event traffic sent from the target VM to the debugger VM request.addClassFilter(className); request.enable(); classPrepareRequests.put(className, request); } LOG.debug("Deferred breakpoint: {}", breakpoint.getLocation().toString()); } @Override public List<Breakpoint> getAllBreakpoints() throws DebuggerException { List<BreakpointRequest> breakpointRequests; try { breakpointRequests = getEventManager().breakpointRequests(); } catch (DebuggerException e) { Throwable cause = e.getCause(); if (cause instanceof VMCannotBeModifiedException) { // If target VM in read-only state then list of break point always empty. return Collections.emptyList(); } throw e; } List<Breakpoint> breakPoints = new ArrayList<>(breakpointRequests.size()); for (BreakpointRequest breakpointRequest : breakpointRequests) { com.sun.jdi.Location location = breakpointRequest.location(); // Breakpoint always enabled at the moment. Managing states of breakpoint is not supported for now. breakPoints.add(newDto(BreakpointDto.class).withEnabled(true) .withLocation(newDto(LocationDto.class).withTarget(location.declaringType().name()) .withLineNumber(location.lineNumber()))); } Collections.sort(breakPoints, BREAKPOINT_COMPARATOR); return breakPoints; } private static final Comparator<Breakpoint> BREAKPOINT_COMPARATOR = new BreakPointComparator(); @Override public void deleteBreakpoint(Location location) throws DebuggerException { final String className = location.getTarget(); final int lineNumber = location.getLineNumber(); EventRequestManager requestManager = getEventManager(); List<BreakpointRequest> snapshot = new ArrayList<>(requestManager.breakpointRequests()); for (BreakpointRequest breakpointRequest : snapshot) { com.sun.jdi.Location jdiLocation = breakpointRequest.location(); if (jdiLocation.declaringType().name().equals(className) && jdiLocation.lineNumber() == lineNumber) { requestManager.deleteEventRequest(breakpointRequest); LOG.debug("Delete breakpoint: {}", location); } } } @Override public void deleteAllBreakpoints() throws DebuggerException { getEventManager().deleteAllBreakpoints(); } @Override public void resume(ResumeAction action) throws DebuggerException { lock.lock(); try { invalidateCurrentThread(); vm.resume(); LOG.debug("Resume VM"); } catch (VMCannotBeModifiedException e) { throw new DebuggerException(e.getMessage(), e); } finally { lock.unlock(); } } @Override public StackFrameDumpDto dumpStackFrame() throws DebuggerException { lock.lock(); try { final JdiStackFrame currentFrame = getCurrentFrame(); StackFrameDumpDto dump = newDto(StackFrameDumpDto.class); boolean existInformation = true; JdiLocalVariable[] variables = new JdiLocalVariable[0]; try { variables = currentFrame.getLocalVariables(); } catch (DebuggerAbsentInformationException e) { existInformation = false; } for (JdiField f : currentFrame.getFields()) { List<String> variablePath = asList(f.isStatic() ? "static" : "this", f.getName()); dump.getFields().add(newDto(FieldDto.class).withIsFinal(f.isFinal()) .withIsStatic(f.isStatic()) .withIsTransient(f.isTransient()) .withIsVolatile(f.isVolatile()) .withName(f.getName()) .withExistInformation(existInformation) .withValue(f.getValue().getAsString()) .withType(f.getTypeName()) .withVariablePath(newDto(VariablePathDto.class).withPath(variablePath)) .withPrimitive(f.isPrimitive())); } for (JdiLocalVariable var : variables) { dump.getVariables().add(newDto(VariableDto.class).withName(var.getName()) .withExistInformation(existInformation) .withValue(var.getValue().getAsString()) .withType(var.getTypeName()) .withVariablePath( newDto(VariablePathDto.class) .withPath(singletonList(var.getName())) ) .withPrimitive(var.isPrimitive())); } return dump; } finally { lock.unlock(); } } /** * Get value of variable with specified path. Each item in path is name of variable. * <p> * Path must be specified according to the following rules: * <ol> * <li>If need to get field of this object of current frame then first element in array always should be * 'this'.</li> * <li>If need to get static field in current frame then first element in array always should be 'static'.</li> * <li>If need to get local variable in current frame then first element should be the name of local variable.</li> * </ol> * </p> * Here is example. <br/> * Assume we have next hierarchy of classes and breakpoint set in line: <i>// breakpoint</i>: * <pre> * class A { * private String str; * ... * } * * class B { * private A a; * .... * * void method() { * A var = new A(); * var.setStr(...); * a = var; * // breakpoint * } * } * </pre> * There are two ways to access variable <i>str</i> in class <i>A</i>: * <ol> * <li>Through field <i>a</i> in class <i>B</i>: ['this', 'a', 'str']</li> * <li>Through local variable <i>var</i> in method <i>B.method()</i>: ['var', 'str']</li> * </ol> * * @param variablePath * path to variable * @return variable or <code>null</code> if variable not found * @throws DebuggerException * when any other errors occur when try to access the variable */ @Override public SimpleValue getValue(VariablePath variablePath) throws DebuggerException { List<String> path = variablePath.getPath(); if (path.size() == 0) { throw new IllegalArgumentException("Path to value may not be empty. "); } JdiVariable variable; int offset; if ("this".equals(path.get(0)) || "static".equals(path.get(0))) { if (path.size() < 2) { throw new IllegalArgumentException("Name of field required. "); } variable = getCurrentFrame().getFieldByName(path.get(1)); offset = 2; } else { try { variable = getCurrentFrame().getLocalVariableByName(path.get(0)); } catch (DebuggerAbsentInformationException e) { return null; } offset = 1; } for (int i = offset; variable != null && i < path.size(); i++) { variable = variable.getValue().getVariableByName(path.get(i)); } if (variable == null) { return null; } List<Variable> variables = new ArrayList<>(); for (JdiVariable ch : variable.getValue().getVariables()) { VariablePathDto chPath = newDto(VariablePathDto.class).withPath(new ArrayList<>(path)); chPath.getPath().add(ch.getName()); if (ch instanceof JdiField) { JdiField f = (JdiField)ch; variables.add(new FieldImpl(f.getName(), true, f.getValue().getAsString(), f.getTypeName(), f.isPrimitive(), Collections.<Variable>emptyList(), chPath, f.isFinal(), f.isStatic(), f.isTransient(), f.isVolatile())); } else { // Array element. variables.add(new VariableImpl(ch.getTypeName(), ch.getName(), ch.getValue().getAsString(), ch.isPrimitive(), chPath, Collections.emptyList(), true)); } } return new SimpleValueImpl(variables, variable.getValue().getAsString()); } @Override public void setValue(Variable variable) throws DebuggerException { StringBuilder expression = new StringBuilder(); for (String s : variable.getVariablePath().getPath()) { if ("static".equals(s)) { continue; } // Here we need !s.startsWith("[") condition because // we shouldn't add '.' between arrayName and index of a element // For example we can receive ["arrayName", "[index]"] if (expression.length() > 0 && !s.startsWith("[")) { expression.append('.'); } expression.append(s); } expression.append('='); expression.append(variable.getValue()); evaluate(expression.toString()); } @Override public void handleEvents(com.sun.jdi.event.EventSet eventSet) throws DebuggerException { boolean resume = true; try { for (com.sun.jdi.event.Event event : eventSet) { LOG.debug("New event: {}", event); if (event instanceof com.sun.jdi.event.BreakpointEvent) { lock.lock(); try { resume = processBreakPointEvent((com.sun.jdi.event.BreakpointEvent)event); } finally { lock.unlock(); } } else if (event instanceof com.sun.jdi.event.StepEvent) { lock.lock(); try { resume = processStepEvent((com.sun.jdi.event.StepEvent)event); } finally { lock.unlock(); } } else if (event instanceof com.sun.jdi.event.VMDisconnectEvent) { resume = processDisconnectEvent(); } else if (event instanceof com.sun.jdi.event.ClassPrepareEvent) { resume = processClassPrepareEvent((com.sun.jdi.event.ClassPrepareEvent)event); } } } finally { if (resume) { eventSet.resume(); } } } private boolean processBreakPointEvent(com.sun.jdi.event.BreakpointEvent event) throws DebuggerException { setCurrentThread(event.thread()); boolean hitBreakpoint; ExpressionParser parser = (ExpressionParser)event.request().getProperty("org.eclipse.che.ide.java.debug.condition.expression.parser"); if (parser != null) { com.sun.jdi.Value result = evaluate(parser); hitBreakpoint = result instanceof com.sun.jdi.BooleanValue && ((com.sun.jdi.BooleanValue)result).value(); } else { // If there is no expression. hitBreakpoint = true; } if (hitBreakpoint) { com.sun.jdi.Location jdiLocation = event.location(); Location location; try { location = debuggerUtil.getLocation(jdiLocation); } catch (DebuggerException e) { location = new LocationImpl(jdiLocation.declaringType().name(), jdiLocation.lineNumber()); } debuggerCallback.onEvent(new SuspendEventImpl(location)); } // Left target JVM in suspended state if result of evaluation of expression is boolean value and true // or if condition expression is not set. return !hitBreakpoint; } private boolean processStepEvent(com.sun.jdi.event.StepEvent event) throws DebuggerException { setCurrentThread(event.thread()); com.sun.jdi.Location jdiLocation = event.location(); Location location = debuggerUtil.getLocation(jdiLocation); debuggerCallback.onEvent(new SuspendEventImpl(location)); return false; } private boolean processDisconnectEvent() { debuggerCallback.onEvent(new DisconnectEventImpl()); eventsCollector.stop(); return true; } private boolean processClassPrepareEvent(com.sun.jdi.event.ClassPrepareEvent event) throws DebuggerException { setCurrentThread(event.thread()); final String className = event.referenceType().name(); // add deferred breakpoints List<Breakpoint> breakpointsToAdd = deferredBreakpoints.get(className); if (breakpointsToAdd != null) { for (Breakpoint b : breakpointsToAdd) { addBreakpoint(b); } deferredBreakpoints.remove(className); // All deferred breakpoints for className have been already added, // so no need to listen for an appropriate ClassPrepareRequests any more. ClassPrepareRequest request = classPrepareRequests.remove(className); if (request != null) { getEventManager().deleteEventRequest(request); } } return true; } @Override public void stepOver(StepOverAction action) throws DebuggerException { doStep(StepRequest.STEP_OVER); } @Override public void stepInto(StepIntoAction action) throws DebuggerException { doStep(StepRequest.STEP_INTO); } @Override public void stepOut(StepOutAction action) throws DebuggerException { doStep(StepRequest.STEP_OUT); } private void doStep(int depth) throws DebuggerException { lock.lock(); try { clearSteps(); StepRequest request = getEventManager().createStepRequest(getCurrentThread(), StepRequest.STEP_LINE, depth); request.addCountFilter(1); request.enable(); resume(newDto(ResumeActionDto.class)); } finally { lock.unlock(); } } private void clearSteps() throws DebuggerException { List<StepRequest> snapshot = new ArrayList<>(getEventManager().stepRequests()); for (StepRequest stepRequest : snapshot) { if (stepRequest.thread().equals(getCurrentThread())) { getEventManager().deleteEventRequest(stepRequest); } } } @Override public String evaluate(String expression) throws DebuggerException { com.sun.jdi.Value result = evaluate(ExpressionParser.newInstance(expression)); return result == null ? "null" : result.toString(); } private com.sun.jdi.Value evaluate(ExpressionParser parser) throws DebuggerException { final long startTime = System.currentTimeMillis(); try { return parser.evaluate(new Evaluator(vm, getCurrentThread())); } catch (ExpressionException e) { throw new DebuggerException(e.getMessage(), e); } finally { final long endTime = System.currentTimeMillis(); LOG.debug("==>> Evaluate time: {} ms", (endTime - startTime)); // Evaluation of expression may update state of frame. invalidateCurrentFrame(); } } private ThreadReference getCurrentThread() throws DebuggerException { if (thread == null) { throw new DebuggerException("Target Java VM is not suspended. "); } return thread; } private JdiStackFrame getCurrentFrame() throws DebuggerException { if (stackFrame != null) { return stackFrame; } try { stackFrame = new JdiStackFrameImpl(getCurrentThread().frame(0)); } catch (IncompatibleThreadStateException e) { throw new DebuggerException("Thread is not suspended. ", e); } return stackFrame; } private void setCurrentThread(ThreadReference t) { stackFrame = null; thread = t; } private void invalidateCurrentFrame() { stackFrame = null; } private void invalidateCurrentThread() { this.thread = null; invalidateCurrentFrame(); } private EventRequestManager getEventManager() throws DebuggerException { try { return vm.eventRequestManager(); } catch (VMCannotBeModifiedException e) { throw new DebuggerException(e.getMessage(), e); } } }