/******************************************************************************* * Copyright (c) 2009 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation * Zend Technologies *******************************************************************************/ package org.eclipse.php.internal.debug.core.xdebug.dbgp.model; import static org.eclipse.php.internal.debug.core.model.IVariableFacet.Facet.KIND_LOCAL; import static org.eclipse.php.internal.debug.core.model.IVariableFacet.Facet.KIND_SUPER_GLOBAL; import static org.eclipse.php.internal.debug.core.model.IVariableFacet.Facet.KIND_THIS; import static org.eclipse.php.internal.debug.core.model.IVariableFacet.Facet.VIRTUAL_CLASS; import java.io.UnsupportedEncodingException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.Vector; import org.eclipse.core.resources.*; import org.eclipse.core.runtime.*; import org.eclipse.debug.core.*; import org.eclipse.debug.core.model.*; import org.eclipse.debug.core.sourcelookup.ISourceContainer; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.php.internal.core.phar.PharPath; import org.eclipse.php.internal.debug.core.IPHPDebugConstants; import org.eclipse.php.internal.debug.core.PHPDebugCoreMessages; import org.eclipse.php.internal.debug.core.PHPDebugPlugin; import org.eclipse.php.internal.debug.core.PHPDebugUtil; import org.eclipse.php.internal.debug.core.launching.PHPLaunchUtilities; import org.eclipse.php.internal.debug.core.model.*; import org.eclipse.php.internal.debug.core.model.IPHPDataType.DataType; import org.eclipse.php.internal.debug.core.pathmapper.DebugSearchEngine; import org.eclipse.php.internal.debug.core.pathmapper.PathEntry; import org.eclipse.php.internal.debug.core.pathmapper.PathMapper; import org.eclipse.php.internal.debug.core.pathmapper.PathMapper.Mapping.MappingSource; import org.eclipse.php.internal.debug.core.pathmapper.VirtualPath; import org.eclipse.php.internal.debug.core.sourcelookup.PHPSourceLookupDirector; import org.eclipse.php.internal.debug.core.sourcelookup.containers.PHPCompositeSourceContainer; import org.eclipse.php.internal.debug.core.xdebug.breakpoints.DBGpExceptionBreakpoint; import org.eclipse.php.internal.debug.core.xdebug.breakpoints.DBGpLineBreakpoint; import org.eclipse.php.internal.debug.core.xdebug.dbgp.DBGpBreakpoint; import org.eclipse.php.internal.debug.core.xdebug.dbgp.DBGpBreakpointFacade; import org.eclipse.php.internal.debug.core.xdebug.dbgp.DBGpLogger; import org.eclipse.php.internal.debug.core.xdebug.dbgp.DBGpPreferences; import org.eclipse.php.internal.debug.core.xdebug.dbgp.protocol.Base64; import org.eclipse.php.internal.debug.core.xdebug.dbgp.protocol.DBGpCommand; import org.eclipse.php.internal.debug.core.xdebug.dbgp.protocol.DBGpResponse; import org.eclipse.php.internal.debug.core.xdebug.dbgp.protocol.DBGpUtils; import org.eclipse.php.internal.debug.core.xdebug.dbgp.protocol.EngineTypes; import org.eclipse.php.internal.debug.core.xdebug.dbgp.session.DBGpSession; import org.eclipse.php.internal.debug.core.xdebug.dbgp.session.DBGpSessionHandler; import org.eclipse.php.internal.debug.core.xdebug.dbgp.session.IDBGpSessionListener; import org.eclipse.swt.widgets.Display; import org.w3c.dom.Node; import org.w3c.dom.NodeList; public class DBGpTarget extends DBGpElement implements IPHPDebugTarget, IDBGpDebugTarget, IStep, IBreakpointManagerListener, IDBGpSessionListener { private static class DBGpBreakpointCmd { private String cmd; private DBGpBreakpoint bp; public DBGpBreakpointCmd(String cmd, DBGpBreakpoint bp) { this.cmd = cmd; this.bp = bp; } public String getCmd() { return cmd; } public DBGpBreakpoint getBp() { return bp; } } /** * class to manage breakpoint conditions * */ private static class DBGpBreakpointCondition { private String hitCondition; private String hitValue; private String expression; private int type; public static final int NONE = 0; public static final int HIT = 1; public static final int EXPR = 2; public static final int INVALID = 3; public DBGpBreakpointCondition(DBGpBreakpoint bp) { type = NONE; if (bp.isConditional() && bp.isConditionEnabled()) { // supported // - expression // - hit(condition value) String bpExpression = bp.getExpression().trim(); if (bpExpression.endsWith(")") //$NON-NLS-1$ && (bpExpression.startsWith("hit(") || bpExpression.startsWith("HIT("))) { //$NON-NLS-1$ //$NON-NLS-2$ if (bpExpression.length() > 5) { // support the following formats // - >= x, >=x // - ==x == x // - %x % x type = HIT; String internal = bpExpression.substring(4, bpExpression.length() - 1).trim(); if (internal.startsWith("%")) { //$NON-NLS-1$ hitCondition = "%"; //$NON-NLS-1$ hitValue = internal.substring(1).trim(); } else { hitCondition = internal.substring(0, 2); if (hitCondition.equals("==") || hitCondition.equals(">=")) { //$NON-NLS-1$ //$NON-NLS-2$ hitValue = internal.substring(2).trim(); } else { type = INVALID; } } if (type != INVALID) { try { Integer.parseInt(hitValue); } catch (NumberFormatException nfe) { type = INVALID; } } } else { type = INVALID; } } else if (bpExpression.length() == 0) { type = NONE; expression = bpExpression; } else { type = EXPR; expression = bpExpression; } } } public String getExpression() { return expression; } public String getHitCondition() { return hitCondition; } public String getHitValue() { return hitValue; } public int getType() { return type; } } // used to identify this debug target with the associated // script being debugged. private String sessionID; private String ideKey; private boolean webLaunch = false; private boolean multiSessionManaged = false; // required for EXE target support private IProcess process; private String stopDebugURL; // debug target state private volatile int targetState; private static final int STATE_CREATE = 0; // target creation private static final int STATE_INIT_SESSION_WAIT = 1; // waiting for 1st // session private static final int STATE_STARTED_SUSPENDED = 2; // suspended private static final int STATE_STARTED_RUNNING = 3; // running private static final int STATE_STARTED_SESSION_WAIT = 4; // web launch // waiting for // next session private static final int STATE_TERMINATING = 5; // ASync stop request made private static final int STATE_TERMINATED = 6; // terminated private static final int STATE_DISCONNECTED = 7; // disconnected // the script being run, or initial web script private String projectScript; // The name to return for this debug target. private String name; // launch object private ILaunch launch; // threads private DBGpThread langThread; private IThread[] allThreads; private int currentStackLevel; private IStackFrame[] stackFrames; private IStackFrame[] previousFrames; private IVariable[] currentVariables; private DBGpBreakpointFacade bpFacade; // superglobal variable support, these are immutable, they cannot be changed private IVariable[] superGlobalVars; // used to cache dbgp commands to program while it is running private Vector<DBGpBreakpointCmd> DBGpCmdQueue = new Vector<DBGpBreakpointCmd>(); // dbgp session support private volatile DBGpSession session; private DBGpPreferences sessionPreferences; private Object sessionMutex = new Object(); private Object commandMutex = new Object(); private TimedEvent te = new TimedEvent(); // debug config settings private boolean stopAtStart; private boolean asyncSupported; private boolean stepping; // private int maxChildren = 0; private PathMapper pathMapper = null; // need to have something in case a target is terminated before // a session is initiated to stop a NPE in the debug view private DebugOutput debugOutput = new DebugOutput(); private boolean hasInitialSource = true; private BreakpointSet breakpointSet; /** * Base constructor * */ private DBGpTarget() { super(null); setState(STATE_CREATE); ideKey = DBGpSessionHandler.getInstance().getIDEKey(); allThreads = new IThread[0]; // needs to be defined when target is // added to launch fireCreationEvent(); } /** * Target that handles PHP Exe launches * * @param launch * @param process * @param projectRelativeScript * @param stopAtStart * @throws CoreException */ public DBGpTarget(ILaunch launch, String projectRelativeScript, String ideKey, String sessionID, boolean stopAtStart) { this(); this.stopAtStart = stopAtStart; this.launch = launch; this.projectScript = projectRelativeScript; this.ideKey = ideKey; this.webLaunch = false; this.sessionID = sessionID; this.process = null; // this will be set later this.stopDebugURL = null; // never set } /** * target that handles invocation via a web browser * * @param launch * @param workspaceRelativeScript * @param stopDebugURL * @param sessionID * @param stopAtStart */ public DBGpTarget(ILaunch launch, String workspaceRelativeScript, String stopDebugURL, String ideKey, String sessionID, boolean stopAtStart) { this(); this.stopAtStart = stopAtStart; this.launch = launch; this.projectScript = workspaceRelativeScript; this.ideKey = ideKey; this.webLaunch = true; this.sessionID = sessionID; this.stopDebugURL = stopDebugURL; this.process = null; } /** * wait for the initial dbgp session to be established * * @param launchMonitor */ public void waitForInitialSession(DBGpBreakpointFacade facade, DBGpPreferences sessionPrefs, IProgressMonitor launchMonitor) { configureInitialState(facade, sessionPrefs); try { while (session == null && !launch.isTerminated() && !isTerminating() && (launchMonitor != null && !launchMonitor.isCanceled())) { // if we got here then session has not been updated // by the other thread yet, so wait. We wait for // an event or a timeout. Even if we timeout we could // still get the session before we re-enter the loop. te.waitForEvent(DBGpPreferences.DBGP_TIMEOUT_DEFAULT); } sessionReceived(launchMonitor); } catch (Exception e) { // cannot proceed any further as we will never be able to get a // session. The exception doesn't need logging. terminateDebugTarget(true); } } public void sessionReceived(DBGpBreakpointFacade facade, DBGpPreferences sessionPrefs) { configureInitialState(facade, sessionPrefs); sessionReceived(null); } public void configureInitialState(DBGpBreakpointFacade facade, DBGpPreferences sessionPrefs) { bpFacade = facade; sessionPreferences = sessionPrefs; setState(STATE_INIT_SESSION_WAIT); } private void sessionReceived(IProgressMonitor launchMonitor) { boolean launchIsCanceled = false; if (session != null && session.isActive()) { if (launchMonitor != null) { launchIsCanceled = launchMonitor.isCanceled(); } if (!isTerminating() && !launch.isTerminated() && !launchIsCanceled) { langThread = new DBGpThread(this); allThreads = new IThread[] { langThread }; langThread.fireCreationEvent(); IBreakpointManager bpmgr = DebugPlugin.getDefault().getBreakpointManager(); bpmgr.addBreakpointListener(this); bpmgr.addBreakpointManagerListener(this); // Determine something about the initial script and path mapping testInitialScriptLocating(); // the pathmapper dialog allows a user to terminate the debug // session. // so check to see if the session has gone away. Could also // check the // state of the target as well if (session != null) { initiateSession(); } } else { session.endSession(); terminateDebugTarget(true); } } else { terminateDebugTarget(true); } } /** * test the initial script to see if we can locate it. If the script is * within the workspace, then we don't need to do anything. If it isn't * check to see if there is a path mapper for it. If not, see if we can * create a path map entry based on the launch information. If we still * cannot do this, prompt the user as we may need info in order to set * breakpoints correctly. TODO: XDebug seemed to accept relative paths as * well as absolute paths, need to investigate further. */ private void testInitialScriptLocating() { String initScript = session.getInitialScript(); if (initScript != null) { // see if the file is in the workspace. IFile file = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(new Path(initScript)); if (file == null) { // ok initial script is not in the workspace // we could do a search or do an automatic path mapping if (pathMapper != null) { if (pathMapper.getLocalFile(initScript) == null) { if (projectScript != null) { // we have a project script so it must be a PDT // launch handlePDTSessionInitiation(initScript); } else { // this was a remotely initiated launch as we don't // have a project script handleRemoteSessionInitiation(initScript); } } } } } } /** * handle a PDT launch debug initiation session * * @param initScript * the initial script being executed */ private void handlePDTSessionInitiation(String initScript) { VirtualPath vpScr = new VirtualPath(projectScript); VirtualPath vpInit = new VirtualPath(initScript); // TODO: What happens if there is a difference in case ? if (vpScr.getLastSegment().equals(vpInit.getLastSegment())) { PathEntry pe = new PathEntry(projectScript, PathEntry.Type.WORKSPACE, ResourcesPlugin.getWorkspace().getRoot()); pathMapper.addEntry(initScript, pe, MappingSource.ENVIRONMENT); } else { // ok, the initial script doesn't match what was passed into // the launch, need to locate the required script. // it may be possible to determine it from the project name // so long as the project name is part of the web server file // structure, so we could try this. // TODO see if the scriptName is part of the init structure, if // so we could workout the local file. try { DebugSearchEngine.find(initScript, this); } catch (Exception e) { } } } /** * handle a Remotely Initiated launch debug session * * @param initScript * the initial script being executed */ private void handleRemoteSessionInitiation(String initScript) { try { PathEntry pe = DebugSearchEngine.find(pathMapper, initScript, null, this); if (pe != null) { Object container = pe.getContainer(); if (container != null && container instanceof IResource) { IResource res = (IResource) container; IProject prj = res.getProject(); PHPSourceLookupDirector dir = (PHPSourceLookupDirector) getLaunch().getSourceLocator(); // ISourceContainer[] containers = new ISourceContainer[] // {new ProjectSourceContainer(prj, false)}; ISourceContainer[] containers = new ISourceContainer[] { new PHPCompositeSourceContainer(prj, null) }; dir.setSourceContainers(containers); } } else { // either no file was found, or the user pressed the stop // debugger if (!isTerminated()) { // stop wasn't pressed hasInitialSource = false; } } } catch (Exception e) { } } /** * initiate the session, this cannot be called from the DBGpSession response * handler thread as we install breakpoints synchronously and block waiting * for the response thread to pick them up, so we will deadlock * */ private void initiateSession() { if (!hasState(STATE_INIT_SESSION_WAIT, STATE_STARTED_SESSION_WAIT)) { DBGpLogger.logWarning("initiateSession in Wrong State: " + targetState, this, null); //$NON-NLS-1$ } stackFrames = null; currentVariables = null; superGlobalVars = null; // Clear any previous debug output object and create a new one. debugOutput = new DebugOutput(); session.startSession(); // We are suspended once the session has handshake until we run setState(STATE_STARTED_SUSPENDED); negotiateDBGpFeatures(); loadPredefinedBreakpoints(); if (!hasInitialSource) { setState(STATE_STARTED_RUNNING); session.sendAsyncCmd(DBGpCommand.run); // Reset initial source flag hasInitialSource = true; return; } if (!stopAtStart) { /* * Set state before issuing a run otherwise a timing window occurs * where a run could suspend, the thread sets state to suspend but * then this thread sets it to running. */ setState(STATE_STARTED_RUNNING); session.sendAsyncCmd(DBGpCommand.run); return; } /* * We have a related source and "Break at First Line" option on. First * say we are suspended on a breakpoint to trigger a perspective switch * then do an initial step into to step onto the 1st line */ suspended(DebugEvent.BREAKPOINT); try { stepInto(); } catch (DebugException e) { } } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDebugTarget#getProcess() */ public IProcess getProcess() { return process; } /** * set the process * * @param proc */ public void setProcess(IProcess proc) { process = proc; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDebugTarget#getThreads() */ public IThread[] getThreads() throws DebugException { if (isTerminated() || hasState(STATE_STARTED_SESSION_WAIT)) return new IThread[0]; return allThreads; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDebugTarget#hasThreads() */ public boolean hasThreads() throws DebugException { return getThreads().length > 0; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDebugTarget#getName() */ public String getName() throws DebugException { if (name == null) { if (isWebLaunch() || multiSessionManaged) { // remote launch name = PHPDebugCoreMessages.XDebug_DBGpTarget_1; } else { if (projectScript == null) { if (session != null) { name = session.getInitialScript(); } else { // Unknown PHP Program name = PHPDebugCoreMessages.XDebug_DBGpTarget_2; } } else { name = this.projectScript; } } } return name; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDebugElement#getDebugTarget() */ public IDebugTarget getDebugTarget() { return this; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDebugElement#getLaunch() */ public ILaunch getLaunch() { return launch; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ITerminate#canTerminate() */ public boolean canTerminate() { return !hasState(STATE_TERMINATED, STATE_CREATE, STATE_DISCONNECTED); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ITerminate#isTerminated() */ public boolean isTerminated() { return hasState(STATE_TERMINATED); } private boolean isTerminating() { return hasState(STATE_TERMINATED, STATE_TERMINATING); } /** * returns is the debug target has started and is not terminating * * @return */ public boolean hasStarted() { return hasState(STATE_STARTED_RUNNING, STATE_STARTED_SESSION_WAIT, STATE_STARTED_SUSPENDED); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ITerminate#terminate() */ public void terminate() throws DebugException { if (isTerminating()) { // just in case we had some problem sending the stop command, allow // terminate to still work. if (session == null && hasState(STATE_TERMINATING)) { terminateDebugTarget(true); } return; } // we won't accept any more sessions, so stop listening DBGpSessionHandler.getInstance().removeSessionListener(this); if (hasState(STATE_STARTED_SUSPENDED)) { // we are suspended, so we can send the stop request to do a clean // termination synchronized (sessionMutex) { if (session != null && session.isActive()) { setState(STATE_TERMINATING); session.sendAsyncCmd(DBGpCommand.stop); // we don't terminateDebugTarget here, we wait for the // response from the program under debug } else { terminateDebugTarget(true); } } } else { // we cannot terminate cleanly so we terminate as best we can terminateDebugTarget(true); if (isWebLaunch()) { // We were a web launch so we must now send the stop url sendStopDebugURL(); } } } /** * Sends stop debug session URL. */ private void sendStopDebugURL() { if (stopDebugURL == null) { return; } DBGpLogger.debug("browser is not null, sending " + stopDebugURL); //$NON-NLS-1$ try { PHPDebugUtil.openLaunchURL(stopDebugURL); } catch (DebugException e) { DBGpLogger.logException("Failed to send stop XDebug session URL: " + stopDebugURL, this, e); //$NON-NLS-1$ } } /** * Called by DBGpSession when the session terminates. The session terminates * if we explicitly stop the session, or the script completes process then * it will be a remote server version, so we don't want to terminate the * debug target, but the session will have ended. This target needs to * either be terminated manually or wait for another debug session to be * attached. */ public void sessionEnded() { boolean unexpectedTermination = false; synchronized (sessionMutex) { session = null; if (hasState(STATE_TERMINATING)) { // we are terminating, if we are a web launch, we need to issue // the // stop URL, then terminate the debug target. if (isWebLaunch() && !isMultiSessionManaged()) { sendStopDebugURL(); } terminateDebugTarget(true); } else { // if we were suspended and we are now terminating then // something // has caused debug to end, most likely a bad eval. unexpectedTermination = isSuspended(); // we were not terminating and the session ended. If we are a // web // launch, then we need to wait for the next session. Otherwise // we // terminate the debug target. if (isWebLaunch() && !isMultiSessionManaged()) { if (isSuspended()) { // if we are suspended, then inform eclipse we have // resumed // so all the user can do is terminate or disconnect // while // waiting for the next session. fireResumeEvent(DebugEvent.RESUME); langThread.fireResumeEvent(DebugEvent.RESUME); } stepping = false; setState(STATE_STARTED_SESSION_WAIT); langThread.setBreakpoints(null); } else { terminateDebugTarget(true); } } } if (unexpectedTermination) { // an unexpected termination occurred, so put out a message. final String errorMessage = PHPDebugCoreMessages.XDebugMessage_unexpectedTermination; Status status = new Status(IStatus.ERROR, PHPDebugPlugin.getID(), IPHPDebugConstants.INTERNAL_ERROR, errorMessage, null); DebugPlugin.log(status); Display.getDefault().asyncExec(new Runnable() { public void run() { MessageDialog.openError(Display.getDefault().getActiveShell(), PHPDebugCoreMessages.XDebugMessage_debugError, errorMessage); } }); } } /** * Terminate this debug target, either because we are terminating the thing * being debugged or we are disconnecting * * @param isTerminate * if we are terminating rather than disconnecting */ public void terminateDebugTarget(boolean isTerminate) { // check we haven't already terminated if (!hasState(STATE_TERMINATED)) { DBGpSessionHandler.getInstance().removeSessionListener(this); IBreakpointManager bpmgr = DebugPlugin.getDefault().getBreakpointManager(); bpmgr.removeBreakpointListener(this); bpmgr.removeBreakpointManagerListener(this); if (isTerminate && hasState(STATE_STARTED_RUNNING)) { setState(STATE_TERMINATING); if (process != null) { try { // terminate the process even if Eclipse may also // attempt this depending on what the user selected // to terminate. process.terminate(); } catch (DebugException e) { // ignore any exceptions here } } else { // this is still required as we could enter // terminateDebugTarget without // session.endSession being called eg when stop debugger is // pressed on the DebugSearchEngine Dialog. if (session != null) { session.endSession(); } } } setState(STATE_TERMINATED); if (session != null) { session.endSession(); } if (langThread != null) { langThread.fireTerminateEvent(); } stepping = false; fireTerminateEvent(); } if (!isMultiSessionManaged()) // Terminate corresponding launch if target is not multi-session // managed try { getLaunch().terminate(); } catch (DebugException e) { } } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IMemoryBlockRetrieval# * supportsStorageRetrieval () */ public boolean supportsStorageRetrieval() { return false; } /* * (non-Javadoc) * * @see * org.eclipse.debug.core.model.IMemoryBlockRetrieval#getMemoryBlock(long, * long) */ public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException { return null; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ISuspendResume#canResume() */ public boolean canResume() { return !isTerminated() && isSuspended(); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ISuspendResume#canSuspend() */ public boolean canSuspend() { return asyncSupported; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ISuspendResume#isSuspended() */ public boolean isSuspended() { return hasState(STATE_STARTED_SUSPENDED); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IStep#canStepInto() */ public boolean canStepInto() { return !isStepping() && isSuspended(); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IStep#canStepOver() */ public boolean canStepOver() { return !isStepping() && isSuspended(); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IStep#canStepReturn() */ public boolean canStepReturn() { // can only step return if there is a method above it, ie there is // at least one stack frame above the current stack frame. try { if (!isStepping() && isSuspended() && getCurrentStackFrames().length > 1) { return true; } } catch (DebugException e) { // ignore the exception if it fails then we cannot stepReturn } return false; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IStep#isStepping() */ public boolean isStepping() { return stepping; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IStep#stepInto() */ public void stepInto() throws DebugException { synchronized (commandMutex) { if (!canStepInto()) return; stepping = true; resumed(DebugEvent.STEP_INTO); } session.sendAsyncCmd(DBGpCommand.stepInto); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IStep#stepOver() */ public void stepOver() throws DebugException { synchronized (commandMutex) { if (!canStepOver()) return; stepping = true; resumed(DebugEvent.STEP_OVER); } session.sendAsyncCmd(DBGpCommand.stepOver); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IStep#stepReturn() */ public void stepReturn() throws DebugException { synchronized (commandMutex) { if (!canStepReturn()) return; stepping = true; resumed(DebugEvent.STEP_RETURN); } session.sendAsyncCmd(DBGpCommand.StepOut); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ISuspendResume#resume() */ public void resume() throws DebugException { synchronized (commandMutex) { if (!canResume()) return; stepping = false; resumed(DebugEvent.RESUME); } // bug in eclipse 3.2. When I issue a resume when a disconnect // is done, the resume button can still be pressed which // wouldn't work as the session has gone. synchronized (sessionMutex) { if (session != null && session.isActive()) { session.sendAsyncCmd(DBGpCommand.run); } } } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.ISuspendResume#suspend() */ public void suspend() throws DebugException { synchronized (sessionMutex) { if (session != null && session.isActive()) { session.sendAsyncCmd(DBGpCommand.suspend); } } } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDisconnect#canDisconnect() */ public boolean canDisconnect() { return hasState(STATE_STARTED_RUNNING, STATE_STARTED_SUSPENDED); } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDisconnect#disconnect() */ public void disconnect() throws DebugException { if (isTerminating()) { return; } if (hasState(STATE_STARTED_RUNNING, STATE_STARTED_SUSPENDED)) { // we are in the middle of a debug session, single or multi // makes no difference, we should stop it setState(STATE_DISCONNECTED); // TODO: May need to synchronize if (session != null) { if (!isWebLaunch()) { // not a web launch, but could be multi session so we // can't just detach if (multiSessionManaged && session.getEngineType() == EngineTypes.Xdebug && versionCheckLT(session.getEngineVersion(), "2.0.2")) { //$NON-NLS-1$ // we have to do a stop if xdebug and < 2.0.2 session.sendSyncCmd(DBGpCommand.stop); } else { session.sendAsyncCmd(DBGpCommand.detach); } terminateDebugTarget(false); } else { // detaching xdebug on apache prior to version 2.0.2 // causes debug to stop working on the server. if (session.getEngineType() == EngineTypes.Xdebug && versionCheckLT(session.getEngineVersion(), "2.0.2")) { //$NON-NLS-1$ // we have to do a stop if xdebug and < 2.0.2 session.sendSyncCmd(DBGpCommand.stop); } else { session.sendAsyncCmd(DBGpCommand.detach); } stepping = false; langThread.setBreakpoints(null); setState(STATE_STARTED_SESSION_WAIT); resumed(DebugEvent.RESUME); } } } } private boolean versionCheckLT(String engineVersion, String requiredVersion) { boolean isLessThan = true; boolean isEqual = true; StringTokenizer stEngine = new StringTokenizer(engineVersion, "."); //$NON-NLS-1$ StringTokenizer stCheck = new StringTokenizer(requiredVersion, "."); //$NON-NLS-1$ while (stEngine.hasMoreTokens()) { String engineValStr = stEngine.nextToken(); if (stCheck.hasMoreTokens()) { String checkValStr = stCheck.nextToken(); try { int engineVal = Integer.parseInt(engineValStr); try { int checkVal = Integer.parseInt(checkValStr); if (engineVal > checkVal) { isLessThan = false; isEqual = false; } if (engineVal != checkVal) { isEqual = false; } } catch (NumberFormatException nfe) { // we are comparing a number to a number followed by // characters // NOT REQUIRED TO BE SUPPORTED } } catch (NumberFormatException nfe) { // we are comparing a number followed by characters with a // number int engineVal = getNumber(engineValStr); isEqual = false; try { int checkVal = Integer.parseInt(checkValStr); if (engineVal > checkVal) { isLessThan = false; isEqual = false; } } catch (NumberFormatException nfe2) { // we are comparing a number to a number followed by // characters // NOT REQUIRED TO BE SUPPORTED } } } } if (stCheck.hasMoreTokens()) { // check has more tokens so if equal so far then 2.0 2.0.(anything) // means // we must be less than isEqual = false; } return isLessThan && !isEqual; } /** * this will only work if there are non digits in there. * * @param engineValStr * @return */ private int getNumber(String engineValStr) { int x = -1; for (int i = 0; i < engineValStr.length(); i++) { char ch = engineValStr.charAt(i); if (!Character.isDigit(ch) && i > 0) { x = Integer.parseInt(engineValStr.substring(0, i)); break; } } return x; } /* * (non-Javadoc) * * @see org.eclipse.debug.core.model.IDisconnect#isDisconnected() */ public boolean isDisconnected() { return hasState(STATE_DISCONNECTED, STATE_TERMINATED); } /** * fire a resume event * * @param detail */ private void resumed(int detail) { setState(STATE_STARTED_RUNNING); fireResumeEvent(detail); langThread.fireResumeEvent(detail); } /** * fire a suspend event * * @param detail */ public void suspended(int detail) { setState(STATE_STARTED_SUSPENDED); processQueuedBpCmds(); previousFrames = stackFrames; stackFrames = null; currentVariables = null; superGlobalVars = null; stepping = false; fireSuspendEvent(detail); langThread.fireSuspendEvent(detail); } /** * setup DBGp specific features, or get information about environment */ private void negotiateDBGpFeatures() { DBGpResponse resp; resp = session.sendSyncCmd(DBGpCommand.featureSet, "-n show_hidden -v 1"); //$NON-NLS-1$ // check the responses, but keep going. DBGpUtils.isGoodDBGpResponse(this, resp); resp = session.sendSyncCmd(DBGpCommand.featureSet, "-n max_depth -v " + getMaxDepth()); //$NON-NLS-1$ DBGpUtils.isGoodDBGpResponse(this, resp); resp = session.sendSyncCmd(DBGpCommand.featureSet, "-n max_children -v " + getMaxChildren()); //$NON-NLS-1$ DBGpUtils.isGoodDBGpResponse(this, resp); resp = session.sendSyncCmd(DBGpCommand.featureSet, "-n max_data -v " + getMaxData()); //$NON-NLS-1$ DBGpUtils.isGoodDBGpResponse(this, resp); resp = session.sendSyncCmd(DBGpCommand.featureGet, "-n encoding"); //$NON-NLS-1$ if (DBGpUtils.isGoodDBGpResponse(this, resp)) { Node child = resp.getParentNode().getFirstChild(); if (child != null) { String data = child.getNodeValue(); try { "abcdefg".getBytes(data); //$NON-NLS-1$ session.setSessionEncoding(data); } catch (UnsupportedEncodingException uee) { DBGpLogger.logWarning("encoding from debug engine invalid", this, uee); //$NON-NLS-1$ } } } asyncSupported = false; resp = session.sendSyncCmd(DBGpCommand.featureGet, "-n supports_async"); //$NON-NLS-1$ if (DBGpUtils.isGoodDBGpResponse(this, resp)) { // TODO: could check the supported atttribute ? // String supportedAttr = DBGpResponse.getAttribute(resp, // "supported"); Node child = resp.getParentNode().getFirstChild(); if (child != null) { String supported = child.getNodeValue(); if (supported != null && supported.equals("1")) { //$NON-NLS-1$ asyncSupported = true; } } } resp = session.sendSyncCmd(DBGpCommand.stdout, "-c " + getCaptureStdout()); //$NON-NLS-1$ DBGpUtils.isGoodDBGpResponse(this, resp); resp = session.sendSyncCmd(DBGpCommand.stderr, "-c " + getCaptureStderr()); //$NON-NLS-1$ DBGpUtils.isGoodDBGpResponse(this, resp); } /** * Returns the current stack frames in the target. * * @return the current stack frames in the target * @throws DebugException * if unable to perform the request */ protected synchronized IStackFrame[] getCurrentStackFrames() throws DebugException { /* * <response command="stack_get" transaction_id="transaction_id"> <stack * level="{NUM}" type="file|eval|?" filename="..." lineno="{NUM}" * where="" cmdbegin="line_number:offset" cmdend="line_number:offset"/> * <stack level="{NUM}" type="file|eval|?" filename="..." * lineno="{NUM}"> <input level="{NUM}" type="file|eval|?" * filename="..." lineno="{NUM}"/> </stack> </response> */ // this can be called from multiple threads, as the data it manages // is global across the debug target, you could end up with 2 threads // doing this at the same time on will be getting the data and the other // will not and returning null as the data is not yet ready. if (stackFrames == null) { currentStackLevel = 0; stackFrames = new IStackFrame[0]; synchronized (sessionMutex) { if (session != null && session.isActive()) { DBGpResponse resp = session.sendSyncCmd(DBGpCommand.stackGet); if (DBGpUtils.isGoodDBGpResponse(this, resp)) { Node parent = resp.getParentNode(); NodeList stackNodes = parent.getChildNodes(); // <stack> entries stackFrames = new IStackFrame[stackNodes.getLength()]; for (int i = 0; i < stackNodes.getLength(); i++) { Node stackNode = stackNodes.item(i); // merge top frame if (i == 0 && previousFrames != null && previousFrames.length != 0) { // merge top frame stackFrames[0] = mergeFrame((DBGpStackFrame) previousFrames[0], new DBGpStackFrame(langThread, stackNode)); continue; } stackFrames[i] = new DBGpStackFrame(langThread, stackNode); } currentStackLevel = stackNodes.getLength() - 1; } } } } return stackFrames; } /** * Merge existing top frame with the incoming one. If both frames have only * different line number then existing is being updated with the use of data * from incoming one. * * @param previous * @param incoming * @return merged frame * @throws DebugException */ private IStackFrame mergeFrame(DBGpStackFrame previous, DBGpStackFrame incoming) throws DebugException { if (previous.getThread() == incoming.getThread() && previous.getName().equals(incoming.getName()) && previous.getStackLevel().equals(incoming.getStackLevel()) && previous.getSourceName().equals(incoming.getSourceName()) && previous.getQualifiedFile().equals(incoming.getQualifiedFile())) { previous.update(incoming.getDescriptor()); return previous; } return incoming; } /** * get the local variables at a particular stack level. Never returns null * (IVariable[0]). * * @param level * @return */ private IVariable[] getContextLocalVars(String level) { DBGpResponse resp = session.sendSyncCmd(DBGpCommand.contextGet, "-d " + level); //$NON-NLS-1$ return parseVarResp(resp, level); } /** * get the super globals. never returns null (IVariable[0]). Cache the info * so that it is never got again when going to other stack levels to view * variables. * * @return */ private IVariable[] getSuperGlobalVars() { if (superGlobalVars == null) { DBGpResponse resp = session.sendSyncCmd(DBGpCommand.contextGet, "-c 1"); //$NON-NLS-1$ // Parse this into a variables block, switch on preload just for // this superGlobalVars = parseVarResp(resp, "-1"); //$NON-NLS-1$ } return superGlobalVars; } /** * get all variables to be displayed. Never returns null (IVariable[0]) * cache the top level stack frame as this is the one most likely always * requested multiple times. * * @param level * @return */ public IVariable[] getVariables(String level) { synchronized (sessionMutex) { if (session != null && session.isActive()) { if (level.equals("0")) { //$NON-NLS-1$ // level "0" is the current stack frame // TODO: we could cache previous level stack frames as well // for // performance in stackframe switching in the future. // TODO: see if preferences have changed about superglobals if (currentVariables == null) { currentVariables = getContextAtLevel(level); return currentVariables; } DBGpLogger.debug("getVariables: returning cached variables"); //$NON-NLS-1$ return currentVariables; } else { return getContextAtLevel(level); } } return new IVariable[0]; } } private IVariable[] getContextAtLevel(String level) { boolean getSuperGlobals = showGLobals(); IVariable[] globals = null; if (getSuperGlobals) { globals = getSuperGlobalVars(); } else { globals = new IVariable[0]; } IVariable[] locals = getContextLocalVars(level); int totalLength = globals.length + locals.length; IVariable[] merged = new IVariable[totalLength]; if (globals.length > 0) { System.arraycopy(globals, 0, merged, 0, globals.length); } if (locals.length > 0) { System.arraycopy(locals, 0, merged, globals.length, locals.length); } setContextFacets(merged); VariablesUtil.sortContextMembers(merged); return merged; } private void setContextFacets(IVariable[] contextVariables) { for (int i = 0; i < contextVariables.length; i++) { if (contextVariables[i] instanceof DBGpVariable) { DBGpVariable dbgpVariable = (DBGpVariable) contextVariables[i]; String endName; try { endName = dbgpVariable.getName(); if (VariablesUtil.isThis(endName)) dbgpVariable.addFacets(KIND_THIS); else if (VariablesUtil.isSuperGlobal(endName)) dbgpVariable.addFacets(KIND_SUPER_GLOBAL); else if (VariablesUtil.isClassIndicator(endName)) dbgpVariable.addFacets(VIRTUAL_CLASS); else dbgpVariable.addFacets(KIND_LOCAL); } catch (DebugException e) { // should not happen } } } } /** * parse each variable request response, never returns null (IVariable[0]) * * @param resp * @param reportedLevel * @return */ private IVariable[] parseVarResp(DBGpResponse resp, String reportedLevel) { // If you cannot get a property, then a single variable is created with // no information as their is a child node, if there are no variables // this method creates a 0 size array which is good. List<DBGpVariable> variables = new ArrayList<DBGpVariable>(); if (DBGpUtils.isGoodDBGpResponse(this, resp) && resp.getErrorCode() == DBGpResponse.ERROR_OK) { Node parent = resp.getParentNode(); NodeList properties = parent.getChildNodes(); for (int i = 0; i < properties.getLength(); i++) { Node property = properties.item(i); if (shouldSkip(property)) continue; variables.add(new DBGpStackVariable(this, property, Integer.valueOf(reportedLevel))); } } return variables.toArray(new DBGpVariable[variables.size()]); } private boolean shouldSkip(Node property) { String type = DBGpResponse.getAttribute(property, "type"); //$NON-NLS-1$ // Skip uninitialized variables if (type.equalsIgnoreCase("uninitialized")) //$NON-NLS-1$ return true; return false; } /** * set a variable to a particular value * * @param fFullName * @param fStackLevel * @param data * @return */ public boolean setProperty(DBGpVariable var, String data) { // XDebug expects all data to be base64 encoded. // In this case we don't use session encoding, we use transfer // encoding as we want control over the bytes being placed into the // variable at the other end. String encoded; try { encoded = Base64.encode(data.getBytes(getBinaryEncoding())); } catch (UnsupportedEncodingException e1) { // should never happen DBGpLogger.logException("unexpected encoding problem", this, e1); //$NON-NLS-1$ encoded = Base64.encode(data.getBytes()); } String fullName = var.getFullName(); String stackLevel = String.valueOf(var.getStackLevel()); String args = "-n " + fullName + " -d " + stackLevel + " -l " + encoded.length() + " -- " + encoded; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ if (var.getDataType() == DataType.PHP_STRING) { // this ensures XDebug doesn't use eval args = "-t string " + args; //$NON-NLS-1$ } DBGpResponse resp = session.sendSyncCmd(DBGpCommand.propSet, args); boolean success = false; if (DBGpUtils.isGoodDBGpResponse(this, resp)) { if (resp.getTopAttribute("success").equals("1")) { //$NON-NLS-1$ //$NON-NLS-2$ if (!stackLevel.equals("0")) { //$NON-NLS-1$ // a variable has been changed on a previous stack // the gui won't have updated the current stack // level view, so we invalidate the cache to reload // the data. The variable also could have been a super // global, so invalid the superglobal cache as well. currentVariables = null; superGlobalVars = null; } success = true; } } return success; } /** * get a variable at a particular stack level and page number * * @param fullName * @param stackLevel * @param page * @return */ public Node getProperty(String fullName, String stackLevel, int page) { if (fullName != null && fullName.trim().length() != 0) { String args = "-n " + fullName + " -d " + stackLevel + " -p " + page; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ if (stackLevel.equals("-1")) { //$NON-NLS-1$ // the following line should work but doesn't in 2.0.0rc1 of // XDebug // args = "-n " + fullName + " -c 1 -p " + page; // but the following works for both rc1 and beyond so will keep // it // like this for now. args = "-n " + fullName + " -d " + getCurrentStackLevel() + " -p " + page; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } DBGpResponse resp = session.sendSyncCmd(DBGpCommand.propGet, args); if (DBGpUtils.isGoodDBGpResponse(this, resp)) { return resp.getParentNode().getFirstChild(); } } // either a bad response or we have a temporary variable from the watch // expression // which we cannot get the results from. return null; } /** * get a variable at a particular stack level and page number * * @param fullName * @param stackLevel * @param page * @return */ public Node getCompleteString(String fullName, String stackLevel, int length) { if (fullName != null && fullName.trim().length() != 0) { String args = "-n " + fullName + " -d " + stackLevel; //$NON-NLS-1$ //$NON-NLS-2$ if (stackLevel.equals("-1")) { //$NON-NLS-1$ // the following line should work but doesn't in 2.0.0rc1 of // XDebug // args = "-n " + fullName + " -c 1 -p " + page; // but the following works for both rc1 and beyond so will keep // it // like this for now. args = "-n " + fullName + " -d " + getCurrentStackLevel(); //$NON-NLS-1$ //$NON-NLS-2$ } // I don't believe the -m option is required for getValue as the // spec says you should use getValue to retrieve the entire data // but xdebug won't work without it. args += " -m " + length; //$NON-NLS-1$ DBGpResponse resp = session.sendSyncCmd(DBGpCommand.propValue, args); if (DBGpUtils.isGoodDBGpResponse(this, resp)) { return resp.getParentNode(); } } // either a bad response or we have a temporary variable from the watch // expression // which we cannot get the results from. return null; } /** * perform an eval request * * @param toEval * @return */ public Node eval(String toEval) { // XDebug expects all data to be base64 encoded. // Convert to session encoding bytes 1st before converting to Base64 String encoded = Base64.encode(getSessionEncodingBytes(toEval)); String args = "-- " + encoded; //$NON-NLS-1$ Node response = null; synchronized (sessionMutex) { if (session != null && session.isActive()) { DBGpResponse resp = session.sendSyncCmd(DBGpCommand.eval, args); if (DBGpUtils.isGoodDBGpResponse(this, resp)) { response = resp.getParentNode().getFirstChild(); } } } return response; } /** * Perform an eval request with result page number. * * @param toEval * @param page * @return */ public Node eval(String toEval, int page) { // XDebug expects all data to be base64 encoded. // Convert to session encoding bytes 1st before converting to Base64 String encoded = Base64.encode(getSessionEncodingBytes(toEval)); String args = " -- " + encoded; //$NON-NLS-1$ Node response = null; synchronized (sessionMutex) { if (session != null && session.isActive()) { DBGpResponse resp = session.sendSyncCmd(DBGpCommand.eval, "-p " + String.valueOf(page) + args); if (DBGpUtils.isGoodDBGpResponse(this, resp)) { response = resp.getParentNode().getFirstChild(); } } } return response; } /** * set the state of the debug target * * @param newState */ private synchronized void setState(int newState) { // TODO: Improvement: build a proper finite state machine with tests if (DBGpLogger.debugState()) { String newStateStr = ""; //$NON-NLS-1$ switch (newState) { case STATE_CREATE: newStateStr = "STATE_CREATE"; //$NON-NLS-1$ break; case STATE_DISCONNECTED: newStateStr = "STATE_DISCONNECTED"; //$NON-NLS-1$ break; case STATE_INIT_SESSION_WAIT: newStateStr = "INIT_SESSION_WAIT"; //$NON-NLS-1$ break; case STATE_STARTED_RUNNING: newStateStr = "STATE_STARTED_RUNNING"; //$NON-NLS-1$ break; case STATE_STARTED_SESSION_WAIT: newStateStr = "STATE_STARTED_SESSION_WAIT"; //$NON-NLS-1$ break; case STATE_STARTED_SUSPENDED: newStateStr = "STATE_STARTED_SUSPENDED"; //$NON-NLS-1$ break; case STATE_TERMINATED: newStateStr = "STATE_TERMINATED"; //$NON-NLS-1$ break; case STATE_TERMINATING: newStateStr = "STATE_TERMINATING"; //$NON-NLS-1$ break; } DBGpLogger.debug("State Change: " + newStateStr); //$NON-NLS-1$ } targetState = newState; } private boolean hasState(int... state) { for (int i = 0; i < state.length; i++) { if (targetState == state[i]) { return true; } } return false; } /** * get current stack depth * * @return */ public int getCurrentStackLevel() { return currentStackLevel; } /* * get the max number of children */ // public int getMaxChildren() { // return maxChildren; // } /** * map the file on this file system to the external one expected by xdebug * 1. file is in the workspace a) use PDT Path mapper workspace definition * b) if no mapping found use external file name and PDT path mapper file * system definition c) if no mapping found then send external file name 2. * file is outside of the workspace a) use PDT Path mapper and PDT path * mapper file system definition b) if no mapping found then send as is * (cannot use Internal Path mapper here) * * @param bp * the breakpoint which references the file to be mapped to an * external file * @return a string representing the external file in absolute format. */ private String mapToExternalFileIfRequired(DBGpBreakpoint bp) { String internalFile = ""; //$NON-NLS-1$ String mappedFileName = null; if (pathMapper != null) { if (bp.getIFile() != null) { // file is defined in the workspace so attempt to map it using // the workspace definition internalFile = bp.getIFile().getFullPath().toString(); mappedFileName = pathMapper.getRemoteFile(internalFile); } if (mappedFileName == null) { // file is not defined in the workspace or no mapping for // workspace file exists // so try to map the fully qualified file. internalFile = bp.getFileName(); mappedFileName = pathMapper.getRemoteFile(internalFile); } } if (mappedFileName == null) { DBGpLogger.debug("outbound File '" + internalFile + "' Not remapped"); //$NON-NLS-1$ //$NON-NLS-2$ mappedFileName = bp.getFileName(); // use the fully qualified // location of the file } else { if (DBGpLogger.debugBP()) { String mapMsg = "remapped eclipse file: '" + internalFile + "' to '" + mappedFileName + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ DBGpLogger.debug(mapMsg); } } return mappedFileName; } /** * map a decoded external absolute file to an absolute one the workspace * will hopefully recognise. rules to decide if mapping is required: * * 1. if the file does exist a) if it mapping found -> remap b) otherwise * don't remap 2. if the file does not exit -> remap * * @param decodedFile * @return absolute path to a workspace registered file. */ public String mapToWorkspaceFileIfRequired(String decodedFile) { String mappedFile = null; PathEntry mappedPathEntry = null; // check to see if the file exists on the file system java.io.File fileSystemFile = new java.io.File(decodedFile); if (fileSystemFile.exists() && pathMapper != null) { mappedPathEntry = pathMapper.getLocalFile(decodedFile); } else { // file doesn't exist so we must remap it, using the PDT path mapper // which could end up prompting the user to create a mapping try { if (projectScript != null) { mappedPathEntry = DebugSearchEngine.find(decodedFile, this); } else { mappedPathEntry = DebugSearchEngine.find(pathMapper, decodedFile, null, this); } } catch (Exception e1) { } } // do we now have a remapped file ? if (mappedPathEntry == null) { final PharPath pharPath = PharPath.getPharPath(new Path("phar:" + decodedFile)); //$NON-NLS-1$ if (pharPath != null) { DBGpLogger.debug("inbound File '" + decodedFile + "' remapped to phar file"); //$NON-NLS-1$ //$NON-NLS-2$ mappedFile = "phar:" + decodedFile; //$NON-NLS-1$ } else { DBGpLogger.debug("inbound File '" + decodedFile + "' Not remapped"); //$NON-NLS-1$ //$NON-NLS-2$ mappedFile = decodedFile; } } else { mappedFile = mappedPathEntry.getResolvedPath(); IResource file = ResourcesPlugin.getWorkspace().getRoot().findMember(new Path(mappedFile)); if (file != null) { // changed as RSE resources return null for RawLocation. IPath t = file.getRawLocation(); if (t != null) { mappedFile = t.toString(); } else { mappedFile = file.getFullPath().toOSString(); } } if (DBGpLogger.debugResp()) { String mapMsg = "mapped inbound file '" + decodedFile + "' to '" + mappedFile + "'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ DBGpLogger.debug(mapMsg); } } return mappedFile; } /* * (non-Javadoc) * * @see * org.eclipse.debug.core.model.IDebugTarget#supportsBreakpoint(org.eclipse * .debug.core.model.IBreakpoint) */ public boolean supportsBreakpoint(IBreakpoint breakpoint) { if (breakpoint.getModelIdentifier().equals(IPHPDebugConstants.ID_PHP_DEBUG_CORE)) { if (breakpoint instanceof IPHPExceptionBreakpoint) return true; boolean support = getBreakpointSet().supportsBreakpoint(breakpoint); return support; } return false; } /* * (non-Javadoc) * * @see * org.eclipse.debug.core.IBreakpointListener#breakpointAdded(org.eclipse * .debug.core.model.IBreakpoint) */ public void breakpointAdded(IBreakpoint breakpoint) { // attempt to add a breakpoint under the following conditions // 1. breakpoint manager enabled // 2. the breakpoint is valid for the environment // 3. the breakpoint is enabled // 4. the debugee is suspended or running and async is supported (send // immediately) // 5. the debugee is running and async not supported (defer and send // later) // otherwise do not send or defer the breakpoint if (!DebugPlugin.getDefault().getBreakpointManager().isEnabled()) { return; } if (supportsBreakpoint(breakpoint)) { try { if (breakpoint.isEnabled()) { DBGpBreakpoint bp = bpFacade.createDBGpBreakpoint(breakpoint); if (isSuspended() || (asyncSupported && isRunning())) { // we are suspended or async mode is supported and we // are running, so send the breakpoint. if (DBGpLogger.debugBP()) { DBGpLogger.debug("Breakpoint Add requested immediately"); //$NON-NLS-1$ } sendBreakpointAddCmd(bp); } else if (isRunning()) { // we are running and async mode is not supported // If we send a breakpoint command we may not get a // response at all or may get a response when the script // suspends on another breakpoint which will hang the // gui until then if we send it synchronously. // Async would require the read thread to handle // the response, locate the breakpoint from the txn_id // and add the id to the runtimeBreakpoint. // We cannot guarantee that the debug server will hold // the request until the script suspends and even queue // multiple requests, so the best bet is to // queue the requests until we suspend. if (DBGpLogger.debugBP()) { DBGpLogger.debug("Breakpoint Add deferred until suspended"); //$NON-NLS-1$ } DBGpBreakpointCmd bpSet = new DBGpBreakpointCmd(DBGpCommand.breakPointSet, bp); queueBpCmd(bpSet); } } } catch (CoreException e) { DBGpLogger.logException("Exception adding breakpoint", this, e); //$NON-NLS-1$ } } } /** * create and send the breakpoint add command * * @param bp * @param onResponseThread */ private void sendBreakpointAddCmd(DBGpBreakpoint bp) { String args = ""; //$NON-NLS-1$ String debugMsg = null; // Check if it is standard line breakpoint if (bp instanceof DBGpLineBreakpoint) { String fileName = bp.getFileName(); int lineNumber = bp.getLineNumber(); // create the add breakpoint command if (DBGpLogger.debugBP()) { debugMsg = "adding breakpoint to file:" + fileName + ", at Line Number: " + lineNumber; //$NON-NLS-1$ //$NON-NLS-2$ } fileName = mapToExternalFileIfRequired(bp); args = "-t line -f " + DBGpUtils.getFileURIString(fileName) + " -n " + lineNumber; //$NON-NLS-1$ //$NON-NLS-2$ } // Check if it is an exception breakpoint else if (bp instanceof DBGpExceptionBreakpoint) { args = MessageFormat.format("-t exception -x {0}", bp.getException()); //$NON-NLS-1$ } // Add condition data if there is any bp.resetConditionChanged(); DBGpBreakpointCondition condition = new DBGpBreakpointCondition(bp); if (condition.getType() == DBGpBreakpointCondition.EXPR) { if (debugMsg != null) { debugMsg += " with expression:" + condition.getExpression(); //$NON-NLS-1$ } // we use session encoding before converting to Base64. args += " -- " + Base64.encode(getSessionEncodingBytes(condition.getExpression())); //$NON-NLS-1$ } else if (condition.getType() == DBGpBreakpointCondition.HIT) { if (debugMsg != null) { debugMsg += " with hit :" + condition.getHitCondition() + condition.getHitValue(); //$NON-NLS-1$ } args += " -h " + condition.getHitValue() + " -o " + condition.hitCondition; //$NON-NLS-1$ //$NON-NLS-2$ } if (debugMsg != null) { DBGpLogger.debug(debugMsg); } DBGpResponse resp; resp = session.sendSyncCmd(DBGpCommand.breakPointSet, args); if (DBGpUtils.isGoodDBGpResponse(this, resp)) { /* * <response command="breakpoint_set" * transaction_id="TRANSACTION_ID" state="STATE" * id="BREAKPOINT_ID"/> */ String bpId = resp.getTopAttribute("id"); //$NON-NLS-1$ if (bp instanceof DBGpLineBreakpoint) { bp.setID(Integer.parseInt(bpId)); } else if (bp instanceof DBGpExceptionBreakpoint) { ((IPHPExceptionBreakpoint) bp.getBreakpoint()).setId(this, Integer.parseInt(bpId)); } if (DBGpLogger.debugBP()) { DBGpLogger.debug("Breakpoint installed with id: " + bpId); //$NON-NLS-1$ } } else { // we have already logged the issue as an error } } /* * (non-Javadoc) * * @see * org.eclipse.debug.core.IBreakpointListener#breakpointRemoved(org.eclipse * .debug.core.model.IBreakpoint, org.eclipse.core.resources.IMarkerDelta) */ public void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) { if (supportsBreakpoint(breakpoint)) { DBGpBreakpoint bp = bpFacade.createDBGpBreakpoint(breakpoint); if (isSuspended() || (asyncSupported && isRunning())) { // aysnc mode and running or we are suspended so send the remove // request if (DBGpLogger.debugBP()) { DBGpLogger.debug("Immediately removing of breakpoint with ID: " + bp.getID()); //$NON-NLS-1$ } sendBreakpointRemoveCmd(bp); } else if (isRunning()) { // running and not suspended and no async support, so we must // defer the removal. if (DBGpLogger.debugBP()) { DBGpLogger.debug("Deferring Removing of breakpoint with ID: " + bp.getID()); //$NON-NLS-1$ } DBGpBreakpointCmd bpRemove = new DBGpBreakpointCmd(DBGpCommand.breakPointRemove, bp); queueBpCmd(bpRemove); } } } /** * create and send the breakpoint remove command * * @param bp * @param onResponseThread */ private void sendBreakpointRemoveCmd(DBGpBreakpoint bp) { int bpId = -1; if (bp instanceof DBGpLineBreakpoint) { bpId = bp.getID(); } else if (bp instanceof DBGpExceptionBreakpoint) { bpId = ((IPHPExceptionBreakpoint) bp.getBreakpoint()).getId(this); } // we are suspended String args = "-d " + bpId; //$NON-NLS-1$ if (DBGpLogger.debugBP()) { DBGpLogger.debug("Removing breakpoint with ID: " + bp.getID()); //$NON-NLS-1$ } DBGpResponse resp; resp = session.sendSyncCmd(DBGpCommand.breakPointRemove, args); DBGpUtils.isGoodDBGpResponse(this, resp); // used to log the // result } /* * (non-Javadoc) * * @see * org.eclipse.debug.core.IBreakpointListener#breakpointChanged(org.eclipse * .debug.core.model.IBreakpoint, org.eclipse.core.resources.IMarkerDelta) */ public void breakpointChanged(IBreakpoint breakpoint, IMarkerDelta delta) { IBreakpointManager bmgr = DebugPlugin.getDefault().getBreakpointManager(); if (!bmgr.isEnabled()) { return; } int deltaLNumber = delta.getAttribute(IMarker.LINE_NUMBER, 0); IMarker marker = breakpoint.getMarker(); int lineNumber = marker.getAttribute(IMarker.LINE_NUMBER, 0); if (supportsBreakpoint(breakpoint)) { try { // did the condition change ? DBGpBreakpoint bp = bpFacade.createDBGpBreakpoint(breakpoint); if (bp.hasConditionChanged()) { if (DBGpLogger.debugBP()) { DBGpLogger.debug("condition changed for breakpoint with ID: " + bp.getID()); //$NON-NLS-1$ } bp.resetConditionChanged(); if (breakpoint.isEnabled()) { breakpointRemoved(breakpoint, null); } else { return; } } // did the line number change ? if (lineNumber != deltaLNumber) { if (DBGpLogger.debugBP()) { DBGpLogger.debug("line number changed for breakpoint with ID: " + bp.getID()); //$NON-NLS-1$ } if (breakpoint.isEnabled()) { breakpointRemoved(breakpoint, null); } else { return; } } // add or remove the break point depending on whether it was // enabled or not if (breakpoint.isEnabled()) { breakpointAdded(breakpoint); } else { breakpointRemoved(breakpoint, null); } } catch (CoreException e) { DBGpLogger.logException("Exception Changing Breakpoint", this, e); //$NON-NLS-1$ } } } /** * Notification a breakpoint was encountered. Determine which breakpoint was * hit and fire a suspend event. * * @param event * debug event */ public void breakpointHit(String filename, int lineno, String exception) { // useful method to be called by the response listener when a // break point has occurred IBreakpoint breakpoint = findBreakpointHit(filename, lineno, exception); if (breakpoint != null) { if (breakpoint instanceof IPHPExceptionBreakpoint) { IPHPExceptionBreakpoint exceptionBreakpoint = (IPHPExceptionBreakpoint) breakpoint; exceptionBreakpoint.setLine(this, lineno); } langThread.setBreakpoints(new IBreakpoint[] { breakpoint }); } else { // might be a hit caused by "Run To Line" command. langThread.setBreakpoints(new IBreakpoint[0]); } // fire event once everything has been established suspended(DebugEvent.BREAKPOINT); } /** * find which breakpoint we have suspended at * * @param filename * @param lineno * @param exception * @return breakpoint */ private IBreakpoint findBreakpointHit(String filename, int lineno, String exception) { return bpFacade.findBreakpointHit(filename, lineno, exception); } /** * setup the currently defined breakpoints before the execution of the * script. */ private void loadPredefinedBreakpoints() { IBreakpointManager bmgr = DebugPlugin.getDefault().getBreakpointManager(); if (!bmgr.isEnabled()) { return; } IBreakpoint[] breakpoints = bmgr.getBreakpoints(bpFacade.getBreakpointModelID()); for (int i = 0; i < breakpoints.length; i++) { breakpointAdded(breakpoints[i]); } } /* * (non-Javadoc) * * @seeorg.eclipse.debug.core.IBreakpointManagerListener# * breakpointManagerEnablementChanged(boolean) */ public void breakpointManagerEnablementChanged(boolean enabled) { IBreakpoint[] breakpoints = DebugPlugin.getDefault().getBreakpointManager() .getBreakpoints(bpFacade.getBreakpointModelID()); for (int i = 0; i < breakpoints.length; i++) { if (supportsBreakpoint(breakpoints[i])) { if (enabled) { // ((PHPLineBreakpoint)breakpoints[i]).setConditionChanged(false); breakpointAdded(breakpoints[i]); } else { breakpointRemoved(breakpoints[i], null); } } } } /** * queue a breakpoint command to be actioned once the script suspends * * @param bpCmd */ private void queueBpCmd(DBGpBreakpointCmd bpCmd) { // Rules are // 1. a remove can delete a previous add and not be queued // 2. all other removes must be honoured // 3. all adds must be honoured (they cannot delete a remove) // 4. problem with the sequence of add/remove is that the remove // cannot work because the add has not been done yet so a // remove must always rebuild the the argument set. // I hope that if we have removed a breakpoint so long as we // hold the reference we can still use the information such as // marker information ? // the queue must only ever have a single remove, add or remove/add (not // add/remove) // for a specific file and line if (bpCmd.getCmd().equals(DBGpCommand.breakPointRemove)) { // search vector in reverse to see if there is an add that matches // and remove it boolean foundAdd = false; if (DBGpCmdQueue.size() > 0) { for (int i = DBGpCmdQueue.size() - 1; i >= 0 && !foundAdd; i--) { DBGpBreakpointCmd entry = (DBGpBreakpointCmd) DBGpCmdQueue.get(i); if (entry.getCmd().equals(DBGpCommand.breakPointSet)) { if (bpCmd.getBp().getFileName().equals(entry.getBp().getFileName()) && bpCmd.getBp().getLineNumber() == entry.getBp().getLineNumber()) { // ok we have an entry that is an Add, the filename // and lineNumber are // the same so we found the entry so let's remove // it. foundAdd = true; DBGpCmdQueue.remove(i); if (DBGpLogger.debugBP()) { DBGpLogger.debug("removed a breakpoint command: " + entry); //$NON-NLS-1$ } } } } } if (!foundAdd) { // add the remove as no add found DBGpCmdQueue.add(bpCmd); } } else { // always add an add DBGpCmdQueue.add(bpCmd); } } /** * process any queued breakpoint commands * */ private void processQueuedBpCmds() { // we must assume we are running on the Session listener thread so // cannot // use sync commands..... if (DBGpLogger.debugBP()) { DBGpLogger.debug("processing deferred BP cmds"); //$NON-NLS-1$ } for (int i = 0; i < DBGpCmdQueue.size(); i++) { DBGpBreakpointCmd bpCmd = (DBGpBreakpointCmd) DBGpCmdQueue.get(i); if (bpCmd.getCmd().equals(DBGpCommand.breakPointSet)) { sendBreakpointAddCmd(bpCmd.getBp()); } else if (bpCmd.getCmd().equals(DBGpCommand.breakPointRemove)) { sendBreakpointRemoveCmd(bpCmd.getBp()); } } DBGpCmdQueue.clear(); } /* * (non-Javadoc) * * @see org.eclipse.php.xdebug.core.dbgp.session.DBGpSessionListener# * SessionCreated (org.eclipse.php.xdebug.core.session.DBGpSession) */ public boolean SessionCreated(DBGpSession session) { // need to determine if the session is one we want, but only if we // are looking for a session, this session may be for us but if // we already have a session, the debugtarget is would have to be // reset to handle this new session, so safer to ignore it. boolean isMine = false; isMine = DBGpSessionHandler.getInstance().isCorrectSession(session, this); if (isMine) { if (this.session == null && !isTerminating()) { session.setDebugTarget(this); this.session = session; if (hasState(STATE_INIT_SESSION_WAIT, STATE_CREATE)) { // if we are in initial session wait, fire the event to // unblock if we haven't even got that far, fire the event // so that when we do enter initial session wait, we // just go straight through. te.signalEvent(); } else { initiateSession(); } } else { // well it is mine, but I am already handling a session so so it // isn't mine and it will be terminated. isMine = false; } } return isMine; } /** * return the IDEKey * * @return */ public String getIdeKey() { return ideKey; } /** * return the session Id * * @return */ public String getSessionID() { return sessionID; } /** * return if this is a web launch * * @return */ public boolean isWebLaunch() { return webLaunch; } public String getSessionEncoding() { if (session != null) { return session.getSessionEncoding(); } else { return DBGpSession.DEFAULT_SESSION_ENCODING; } } public String getBinaryEncoding() { if (session != null) { return session.getBinaryEncoding(); } else { return DBGpSession.DEFAULT_BINARY_ENCODING; } } private int getMaxDepth() { if (sessionPreferences != null) { return sessionPreferences.getInt(DBGpPreferences.DBGP_MAX_DEPTH_PROPERTY, DBGpPreferences.DBGP_MAX_DEPTH_DEFAULT); } return DBGpPreferences.DBGP_MAX_DEPTH_DEFAULT; } public int getMaxChildren() { if (sessionPreferences != null) { return sessionPreferences.getInt(DBGpPreferences.DBGP_MAX_CHILDREN_PROPERTY, DBGpPreferences.DBGP_MAX_CHILDREN_DEFAULT); } return DBGpPreferences.DBGP_MAX_CHILDREN_DEFAULT; } public int getMaxData() { if (sessionPreferences != null) { return sessionPreferences.getInt(DBGpPreferences.DBGP_MAX_DATA_PROPERTY, DBGpPreferences.DBGP_MAX_DATA_DEFAULT); } return DBGpPreferences.DBGP_MAX_CHILDREN_DEFAULT; } private int getCaptureStdout() { if (sessionPreferences != null) { return sessionPreferences.getInt(DBGpPreferences.DBGP_CAPTURE_STDOUT_PROPERTY, DBGpPreferences.DBGP_CAPTURE_DEFAULT); } return DBGpPreferences.DBGP_CAPTURE_DEFAULT; } private int getCaptureStderr() { if (sessionPreferences != null) { return sessionPreferences.getInt(DBGpPreferences.DBGP_CAPTURE_STDERR_PROPERTY, DBGpPreferences.DBGP_CAPTURE_DEFAULT); } return DBGpPreferences.DBGP_CAPTURE_DEFAULT; } private boolean showGLobals() { if (sessionPreferences != null) { return sessionPreferences.getBoolean(DBGpPreferences.DBGP_SHOW_GLOBALS_PROPERTY, DBGpPreferences.DBGP_SHOW_GLOBALS_DEFAULT); } return DBGpPreferences.DBGP_SHOW_GLOBALS_DEFAULT; } public void setPathMapper(PathMapper pathMapper) { this.pathMapper = pathMapper; } /** * return true if a script is executed, ie in running state. * * @return true if running. */ public boolean isRunning() { return hasState(STATE_STARTED_RUNNING); } public boolean isMultiSessionManaged() { return multiSessionManaged; } public void setMultiSessionManaged(boolean multiSessionManaged) { this.multiSessionManaged = multiSessionManaged; } public DBGpSession getSession() { return session; } public void setSession(DBGpSession session) { this.session = session; } public DebugOutput getOutputBuffer() { return this.debugOutput; } private byte[] getSessionEncodingBytes(String toConvert) { byte[] result = null; try { result = toConvert.getBytes(getSessionEncoding()); } catch (UnsupportedEncodingException e) { DBGpLogger.logException("unexpected encoding problem", this, e); //$NON-NLS-1$ } return result; } public boolean isWaiting() { return hasState(STATE_STARTED_SESSION_WAIT); } private BreakpointSet getBreakpointSet() { if (breakpointSet == null) { breakpointSet = new BreakpointSet(PHPLaunchUtilities.getProject(this), !webLaunch); } return breakpointSet; } }