/******************************************************************************* * Copyright (c) 2016 Rogue Wave Software, Inc. * 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: * Rogue Wave Software, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.zdb.server; import static org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.NOTIFICATION_READY; import static org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.NOTIFICATION_SESSION_STARTED; import static org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.NOTIFICATION_SRIPT_ENDED; import static org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.NOTIFICATION_START_PROCESS_FILE; import static org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.REQUEST_GET_LOCAL_FILE_CONTENT; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; 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.StackFrameDump; 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.DebuggerInfoImpl; import org.eclipse.che.api.debug.shared.model.impl.SimpleValueImpl; import org.eclipse.che.api.debug.shared.model.impl.StackFrameDumpImpl; import org.eclipse.che.api.debug.shared.model.impl.VariablePathImpl; import org.eclipse.che.api.debug.shared.model.impl.event.BreakpointActivatedEventImpl; 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.api.project.server.VirtualFileEntry; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.AddBreakpointRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.AddFilesRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.CloseSessionNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.ContinueProcessFileNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.DeleteAllBreakpointsRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.DeleteBreakpointRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.GetLocalFileContentResponse; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.GoRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.IDbgClientResponse; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.SetProtocolRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.StartRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.StepIntoRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.StepOutRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.StepOverRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgConnection; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgConnection.IEngineMessageHandler; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.AddBreakpointResponse; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.GetLocalFileContentRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.IDbgEngineNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.IDbgEngineRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.IDbgEngineResponse; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.ReadyNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.ScriptEndedNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.SessionStartedNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.SetProtocolResponse; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.StartProcessFileNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgSettings; import org.eclipse.che.plugin.zdb.server.expressions.IDbgExpression; import org.eclipse.che.plugin.zdb.server.expressions.ZendDbgExpression; import org.eclipse.che.plugin.zdb.server.expressions.ZendDbgExpressionEvaluator; import org.eclipse.che.plugin.zdb.server.utils.ZendDbgConnectionUtils; import org.eclipse.che.plugin.zdb.server.utils.ZendDbgFileUtils; import org.eclipse.che.plugin.zdb.server.utils.ZendDbgVariableUtils; import org.eclipse.che.plugin.zdb.server.variables.IDbgVariable; import org.eclipse.che.plugin.zdb.server.variables.ZendDbgVariable; import org.eclipse.che.plugin.zdb.server.variables.ZendDbgVariables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Zend Debugger for PHP. * * @author Bartlomiej Laczkowski */ public class ZendDebugger implements Debugger, IEngineMessageHandler { private static final class VariablesStorage { private static final String GLOBALS_VARIABLE = "$GLOBALS"; private final List<IDbgVariable> variables; public VariablesStorage(List<IDbgVariable> variables) { this.variables = variables; } List<IDbgVariable> getVariables() { return variables; } IDbgVariable findVariable(VariablePath variablePath) { List<IDbgVariable> currentVariables = variables; IDbgVariable matchingVariable = null; Iterator<String> pathIterator = variablePath.getPath().iterator(); while (pathIterator.hasNext()) { String pathElement = pathIterator.next(); for (IDbgVariable currentVariable : currentVariables) { List<String> currentVariablePath = currentVariable.getVariablePath().getPath(); String currentVariablePathElement = currentVariablePath.get(currentVariablePath.size() - 1); if (currentVariablePathElement.equals(pathElement)) { matchingVariable = currentVariable; if (pathIterator.hasNext()) { currentVariables = new ArrayList<>(currentVariable.getVariables()); } break; } } } return matchingVariable; } } private static final class ZendDbgBreakpoint { public static ZendDbgBreakpoint create(Breakpoint vfsBreakpoint, ZendDbgLocationHandler debugLocationHandler) { Location dbgLocation = debugLocationHandler.convertToDBG(vfsBreakpoint.getLocation()); return new ZendDbgBreakpoint(dbgLocation, vfsBreakpoint); } private Location dbgLocation; private Breakpoint vfsBreakpoint; private ZendDbgBreakpoint(Location dbgLocation, Breakpoint vfsBreakpoint) { this.dbgLocation = dbgLocation; this.vfsBreakpoint = vfsBreakpoint; } public Location getLocation() { return dbgLocation; } public Breakpoint getVfsBreakpoint() { return vfsBreakpoint; } } public static final Logger LOG = LoggerFactory.getLogger(ZendDebugger.class); private static final int SUPPORTED_PROTOCOL_ID = 2012121702; private final DebuggerCallback debugCallback; private final ZendDbgSettings debugSettings; private final ZendDbgLocationHandler debugLocationHandler; private final ZendDbgConnection debugConnection; private final ZendDbgExpressionEvaluator debugExpressionEvaluator; private VariablesStorage debugVariableStorage; private String debugStartFile; private Map<Breakpoint, ZendDbgBreakpoint> breakpoints = new LinkedHashMap<>(); private Map<ZendDbgBreakpoint, Integer> breakpointIds = new LinkedHashMap<>(); private Integer breakpointAflId = null; public ZendDebugger(ZendDbgSettings debugSettings, ZendDbgLocationHandler debugLocationHandler, DebuggerCallback debugCallback) throws DebuggerException { this.debugCallback = debugCallback; this.debugSettings = debugSettings; this.debugLocationHandler = debugLocationHandler; this.debugConnection = new ZendDbgConnection(this, debugSettings); this.debugExpressionEvaluator = new ZendDbgExpressionEvaluator(debugConnection); this.debugVariableStorage = new VariablesStorage(Collections.emptyList()); } @Override public void handleNotification(IDbgEngineNotification notification) { switch (notification.getType()) { case NOTIFICATION_SESSION_STARTED: { handleSessionStarted((SessionStartedNotification) notification); break; } case NOTIFICATION_START_PROCESS_FILE: { handleStartProcessFile((StartProcessFileNotification) notification); break; } case NOTIFICATION_READY: { handleReady((ReadyNotification) notification); break; } case NOTIFICATION_SRIPT_ENDED: { handleScriptEnded((ScriptEndedNotification) notification); break; } default: break; } } @SuppressWarnings("unchecked") @Override public <T extends IDbgClientResponse> T handleRequest(IDbgEngineRequest<T> request) { switch (request.getType()) { case REQUEST_GET_LOCAL_FILE_CONTENT: return (T) handleGetLocalFileContent((GetLocalFileContentRequest) request); } return null; } @Override public void start(StartAction action) throws DebuggerException { // Initialize connection daemon thread debugConnection.connect(); for (Breakpoint breakpoint : action.getBreakpoints()) { breakpoints.put(breakpoint, ZendDbgBreakpoint.create(breakpoint, debugLocationHandler)); } LOG.debug("Connect {}:{}", debugSettings.getClientHostIP(), debugSettings.getDebugPort()); } @Override public void disconnect() throws DebuggerException { // Stop connection daemon thread debugConnection.disconnect(); } @Override public DebuggerInfo getInfo() throws DebuggerException { return new DebuggerInfoImpl(debugSettings.getClientHostIP(), debugSettings.getDebugPort(), "Zend Debugger", "", 0, null); } @Override public StackFrameDump dumpStackFrame() { sendGetVariables(); return new StackFrameDumpImpl(Collections.emptyList(), debugVariableStorage.getVariables()); } @Override public SimpleValue getValue(VariablePath variablePath) { IDbgVariable matchingVariable = debugVariableStorage.findVariable(variablePath); matchingVariable.makeComplete(); return new SimpleValueImpl(matchingVariable.getVariables(), matchingVariable.getValue()); } @Override public void addBreakpoint(Breakpoint breakpoint) throws DebuggerException { ZendDbgBreakpoint dbgBreakpoint = ZendDbgBreakpoint.create(breakpoint, debugLocationHandler); breakpoints.put(breakpoint, dbgBreakpoint); sendAddBreakpoint(dbgBreakpoint); } @Override public void deleteBreakpoint(Location location) throws DebuggerException { Breakpoint matchingBreakpoint = null; for (Breakpoint breakpoint : breakpoints.keySet()) { if (breakpoint.getLocation().equals(location)) { matchingBreakpoint = breakpoint; break; } } if (matchingBreakpoint == null) { return; } ZendDbgBreakpoint dbgBreakpoint = breakpoints.remove(matchingBreakpoint); // Unregister breakpoint if it was registered in active session if (breakpointIds.containsKey(dbgBreakpoint)) { int breakpointId = breakpointIds.remove(dbgBreakpoint); sendDeleteBreakpoint(breakpointId); } } @Override public void deleteAllBreakpoints() throws DebuggerException { breakpoints.clear(); breakpointIds.clear(); sendDeleteAllBreakpoints(); } @Override public List<Breakpoint> getAllBreakpoints() throws DebuggerException { return new ArrayList<>(breakpoints.keySet()); } @Override public void stepOver(StepOverAction action) throws DebuggerException { sendStepOver(); } @Override public void stepInto(StepIntoAction action) throws DebuggerException { sendStepInto(); } @Override public void stepOut(StepOutAction action) throws DebuggerException { sendStepOut(); } @Override public void resume(ResumeAction action) throws DebuggerException { sendGo(); } @Override public void setValue(Variable variable) throws DebuggerException { Variable matchingVariable = debugVariableStorage.findVariable(variable.getVariablePath()); ((ZendDbgVariable) matchingVariable).setValue(variable.getValue()); } @Override public String evaluate(String expression) throws DebuggerException { ZendDbgExpression zendDbgExpression = new ZendDbgExpression(debugExpressionEvaluator, expression, Collections.emptyList()); zendDbgExpression.evaluate(); return zendDbgExpression.getValue(); } @Override public String toString() { return "ZendDebugger [clientHostIP=" + debugSettings.getClientHostIP() + ", debugPort=" + debugSettings.getDebugPort() + ", useSsslEncryption=" + debugSettings.isUseSsslEncryption() + "]"; } private void handleSessionStarted(SessionStartedNotification notification) { if (!sendSetProtocol()) { sendCloseSession(); LOG.error("Unsupported protocol version: " + notification.getServerProtocolID() + ", only most recent protocol version: " + SUPPORTED_PROTOCOL_ID + " is supported."); } debugStartFile = notification.getFileName(); if (debugSettings.isBreakAtFirstLine()) { AddBreakpointResponse response = debugConnection .sendRequest(new AddBreakpointRequest(1, 1, -1, debugStartFile)); if (isOK(response)) { breakpointAflId = response.getBreakpointID(); } } sendAddBreakpointFiles(); sendStartSession(); } private void handleStartProcessFile(StartProcessFileNotification notification) { sendAddBreakpoints(notification.getFileName()); sendContinueProcessFile(); } private void handleReady(ReadyNotification notification) { String remoteFilePath = notification.getFileName(); if (breakpointAflId != null && remoteFilePath.equals(debugStartFile)) { debugConnection.sendRequest(new DeleteBreakpointRequest(breakpointAflId)); breakpointAflId = null; } int lineNumber = notification.getLineNumber(); Location dbgLocation = ZendDbgLocationHandler.createDBG(remoteFilePath, lineNumber); // Convert DBG location from engine to VFS location Location vfsLocation = debugLocationHandler.convertToVFS(dbgLocation); // Send suspend event debugCallback.onEvent(new SuspendEventImpl(vfsLocation)); } private void handleScriptEnded(ScriptEndedNotification notification) { sendCloseSession(); } private GetLocalFileContentResponse handleGetLocalFileContent(GetLocalFileContentRequest request) { String remoteFilePath = request.getFileName(); VirtualFileEntry localFileEntry = ZendDbgFileUtils.findVirtualFileEntry(remoteFilePath); if (localFileEntry == null) { LOG.error("Could not found corresponding local file for: " + remoteFilePath); return new GetLocalFileContentResponse(request.getID(), GetLocalFileContentResponse.STATUS_FAILURE, null); } try { byte[] localFileContent = localFileEntry.getVirtualFile().getContentAsBytes(); // Check if remote content is equal to corresponding local one if (ZendDbgConnectionUtils.isRemoteContentEqual(request.getSize(), request.getCheckSum(), localFileContent)) { // Remote and local contents are identical return new GetLocalFileContentResponse(request.getID(), GetLocalFileContentResponse.STATUS_FILES_IDENTICAL, null); } // Remote and local contents are different, send local content to the engine return new GetLocalFileContentResponse(request.getID(), GetLocalFileContentResponse.STATUS_SUCCESS, localFileContent); } catch (Exception e) { LOG.error(e.getMessage(), e); } return new GetLocalFileContentResponse(request.getID(), GetLocalFileContentResponse.STATUS_FAILURE, null); } private void sendStartSession() { debugConnection.sendRequest(new StartRequest()); } private boolean sendSetProtocol() { SetProtocolResponse response = debugConnection.sendRequest(new SetProtocolRequest(SUPPORTED_PROTOCOL_ID)); if (isOK(response)) { return response.getProtocolID() >= SUPPORTED_PROTOCOL_ID; } return false; } private void sendContinueProcessFile() { debugConnection.sendNotification(new ContinueProcessFileNotification()); } private void sendGetVariables() { ZendDbgVariables zendVariablesExpression = new ZendDbgVariables(debugExpressionEvaluator); zendVariablesExpression.evaluate(); List<IDbgVariable> variables = new ArrayList<>(); int variableId = 0; for (IDbgExpression zendVariableExpression : zendVariablesExpression.getChildren()) { if (VariablesStorage.GLOBALS_VARIABLE.equalsIgnoreCase(zendVariableExpression.getExpression())) continue; IDbgVariable variable = new ZendDbgVariable(new VariablePathImpl(String.valueOf(variableId++)), zendVariableExpression); if (ZendDbgVariableUtils.isThis(zendVariableExpression.getExpression())) { // $this always on top variables.add(0, variable); } else { variables.add(variable); } } debugVariableStorage = new VariablesStorage(variables); } private void sendAddBreakpointFiles() { Set<String> breakpointFiles = new HashSet<>(); for (ZendDbgBreakpoint dbgBreakpoint : breakpoints.values()) { breakpointFiles.add(dbgBreakpoint.getLocation().getResourcePath()); } debugConnection.sendRequest(new AddFilesRequest(breakpointFiles)); } private void sendAddBreakpoints(String remoteFilePath) { List<ZendDbgBreakpoint> fileBreakpoints = new ArrayList<>(); for (ZendDbgBreakpoint dbgBreakpoint : breakpoints.values()) { if (dbgBreakpoint.getLocation().getResourcePath().equals(remoteFilePath)) { fileBreakpoints.add(dbgBreakpoint); } } for (ZendDbgBreakpoint dbgBreakpoint : fileBreakpoints) { AddBreakpointResponse response = debugConnection.sendRequest( new AddBreakpointRequest(1, 2, dbgBreakpoint.getLocation().getLineNumber(), remoteFilePath)); if (isOK(response)) { // Breakpoint was successfully registered in active session, send breakpoint activated event breakpointIds.put(dbgBreakpoint, response.getBreakpointID()); debugCallback.onEvent(new BreakpointActivatedEventImpl(dbgBreakpoint.getVfsBreakpoint())); } } } private void sendAddBreakpoint(ZendDbgBreakpoint dbgBreakpoint) { AddBreakpointResponse response = debugConnection.sendRequest(new AddBreakpointRequest(1, 2, dbgBreakpoint.getLocation().getLineNumber(), dbgBreakpoint.getLocation().getResourcePath())); if (isOK(response)) { // Breakpoint was successfully registered in active session, send breakpoint activated event breakpointIds.put(dbgBreakpoint, response.getBreakpointID()); debugCallback.onEvent(new BreakpointActivatedEventImpl(dbgBreakpoint.getVfsBreakpoint())); } } private void sendDeleteBreakpoint(int breakpointId) { debugConnection.sendRequest(new DeleteBreakpointRequest(breakpointId)); } private void sendDeleteAllBreakpoints() { debugConnection.sendRequest(new DeleteAllBreakpointsRequest()); } private void sendStepOver() { debugConnection.sendRequest(new StepOverRequest()); } private void sendStepInto() { debugConnection.sendRequest(new StepIntoRequest()); } private void sendStepOut() { debugConnection.sendRequest(new StepOutRequest()); } private void sendGo() { debugConnection.sendRequest(new GoRequest()); } private void sendCloseSession() { debugConnection.sendNotification(new CloseSessionNotification()); } private boolean isOK(IDbgEngineResponse response) { return response != null && response.getStatus() == 0; } }