/*******************************************************************************
* 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.session;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.php.debug.core.debugger.parameters.IDebugParametersKeys;
import org.eclipse.php.internal.debug.core.preferences.PHPProjectPreferences;
import org.eclipse.php.internal.debug.core.xdebug.dbgp.DBGpLogger;
import org.eclipse.php.internal.debug.core.xdebug.dbgp.model.DBGpTarget;
import org.eclipse.php.internal.debug.core.xdebug.dbgp.protocol.*;
import org.w3c.dom.Node;
/**
* DBGp session.
*/
public class DBGpSession {
/**
* Reads all responses from DBGp based debugger, this runs on a background
* job thread.
*/
private class ResponseReader extends Job {
public ResponseReader() {
super("DBGp Response Reader"); //$NON-NLS-1$
setSystem(true);
}
@Override
protected IStatus run(IProgressMonitor monitor) {
byte[] response = null;
while (sessionActive) {
/*
* Here we need to block waiting for a response then process
* that response by related handler.
*/
try {
response = readResponse();
if (response != null) {
DBGpResponse parsedResponse = new DBGpResponse();
parsedResponse.parseResponse(response);
int respErrorCode = parsedResponse.getErrorCode();
/*
* We have received something back from the debugger so
* first we try to process a stop or break async
* response, even if the response was invalid.
*/
if (respErrorCode == DBGpResponse.ERROR_OK
|| respErrorCode == DBGpResponse.ERROR_INVALID_RESPONSE) {
int respType = parsedResponse.getType();
if (respType == DBGpResponse.RESPONSE) {
if (parsedResponse.getStatus().equals(DBGpResponse.STATUS_STOPPED)) {
(new ResponseHandler()).perform(ResponseHandlerAction.HANDLE_STOP, parsedResponse);
} else if (parsedResponse.getStatus().equals(DBGpResponse.STATUS_BREAK)) {
(new ResponseHandler()).perform(ResponseHandlerAction.HANDLE_BREAK, parsedResponse);
} else if (parsedResponse.getStatus().equals(DBGpResponse.STATUS_STOPPING)) {
(new ResponseHandler()).perform(ResponseHandlerAction.HANDLE_STOPPING,
parsedResponse);
}
} else if (respType == DBGpResponse.STREAM
&& respErrorCode != DBGpResponse.ERROR_INVALID_RESPONSE) {
(new ResponseHandler()).perform(ResponseHandlerAction.HANDLE_STREAM, parsedResponse);
} else {
DBGpLogger.logWarning("Unknown type of XML: " //$NON-NLS-1$
+ response, DBGpSession.this, null);
}
}
/*
* Unblock any Sync caller who might be waiting
* regardless of what we got back.
*/
unblockSyncCaller(parsedResponse);
}
} catch (Throwable t) {
DBGpLogger.logException("Unexpected exception. Terminating the debug session", //$NON-NLS-1$
this, t);
}
}
/*
* If the socket is closed or the session terminated then we inform
* the debug target.
*/
try {
/*
* Wait a very brief period to ensure console displays
* everything before stating the debug session has ended.
*/
Thread.sleep(50);
} catch (InterruptedException e) {
}
/*
* End the session here as we most likely terminated cleanly. It
* doesn't matter if endSession is called multiple times.
*/
endSession();
return Status.OK_STATUS;
}
/**
* unblock a sync caller
*
* @param parsedResponse
*/
private void unblockSyncCaller(DBGpResponse parsedResponse) {
/*
* Look to see if another thread is waiting for this response, if
* not then the response is lost must protect if the response
* doesn't include a txn id.
*/
Integer idObj = null;
try {
idObj = Integer.valueOf(parsedResponse.getId());
} catch (NumberFormatException nfe) {
idObj = Integer.valueOf(DBGpCmd.getLastIdSent());
if (DBGpLogger.debugResp()) {
DBGpLogger.debug("no txn id, using last which was" //$NON-NLS-1$
+ idObj.toString());
}
}
if (savedResponses.containsKey(idObj) && parsedResponse.getType() == DBGpResponse.RESPONSE) {
postAndSignalCaller(idObj, parsedResponse);
} else {
/*
* No one waiting for the response, so we need to check the
* response was ok and generate log info. This could have been a
* response to an async invocation.
*/
DBGpUtils.isGoodDBGpResponse(this, parsedResponse);
}
}
}
/**
* Handles action related to given response type.
*/
private class ResponseHandler extends Job {
private ResponseHandlerAction actionType;
private DBGpResponse response;
public ResponseHandler() {
super("DBGp Response Handler"); //$NON-NLS-1$
setSystem(true);
setUser(false);
// Should be performed one after another
setRule(schedulingRule);
}
@Override
protected IStatus run(IProgressMonitor monitor) {
switch (actionType) {
case HANDLE_BREAK: {
handleBreak();
break;
}
case HANDLE_STREAM: {
handleStream();
break;
}
case HANDLE_STOPPING:
case HANDLE_STOP: {
handleStop();
break;
}
default:
break;
}
return Status.OK_STATUS;
}
void perform(ResponseHandlerAction actionType, DBGpResponse response) {
this.actionType = actionType;
this.response = response;
schedule();
}
private void handleStream() {
// OK, we need to put the stream somewhere
String data = response.getStreamData();
if (data != null) {
byte[] streamData = Base64.decode(data);
String streamStr;
try {
streamStr = new String(streamData, outputEncoding);
} catch (UnsupportedEncodingException e) {
DBGpLogger.logException("invalid encoding: " //$NON-NLS-1$
+ outputEncoding, this, e);
streamStr = new String(streamData);
}
// Debug target might be already disconnected
if (debugTarget != null) {
debugTarget.getOutputBuffer().append(streamStr);
}
}
}
/**
* script has stopped, either by request or reached the end
*
*/
private void handleStop() {
endSession();
}
/**
* script has suspended
*
* @param parsedResponse
*/
private void handleBreak() {
// Handle the break status response information.
// this occurs when:
// 1. a break point is hit
// 2. a step command ends and we are suspended
// 3. a command has failed, you get the status = break, reason=ok,
// then you get the error information
if (response.getStatus().equals(DBGpResponse.STATUS_BREAK)) {
Node breakData = response.getParentNode().getFirstChild();
String exception = DBGpResponse.getAttribute(breakData, "exception"); //$NON-NLS-1$
/*
* We have suspended, so now we can go off and handle
* outstanding breakpoint requests
* debugTarget.processDBGpQueuedCmds();
*/
if (response.getReason().equals(DBGpResponse.REASON_OK)) {
// we have hit a breakpoint, or completed a step
String cmd = response.getCommand();
if (cmd.equals(DBGpCommand.run) || !exception.isEmpty()) {
/*
* OK we hit a break point somewhere, we need to get the
* stack information to find out which breakpoint we hit
* as no info is provided in the response. We cannot use
* the DBGpTarget version here as we do an async call.
* Plus we need to handle the possibility of
* STATUS_STOPPED being returned.
*/
response = sendSyncCmd(DBGpCommand.stackGet, null);
if (response != null) {
/*
* We could have received a stop here so we need to
* check for this.
*/
if (response.getStatus().equals(DBGpResponse.STATUS_STOPPED)) {
handleStop();
} else {
Node stackData = response.getParentNode().getFirstChild();
String line = DBGpResponse.getAttribute(stackData, "lineno"); //$NON-NLS-1$
int lineno = 0;
try {
lineno = Integer.parseInt(line);
String filename = DBGpUtils
.getFilenameFromURIString(DBGpResponse.getAttribute(stackData, "filename")); //$NON-NLS-1$
filename = debugTarget.mapToWorkspaceFileIfRequired(filename);
// Debug target might be already
// disconnected
if (debugTarget != null) {
debugTarget.breakpointHit(filename, lineno, exception);
}
} catch (NumberFormatException nfe) {
DBGpLogger.logException("Unexpected number format exception", //$NON-NLS-1$
this, nfe);
}
}
}
} else if (cmd.equals(DBGpCommand.stepInto) || cmd.equals(DBGpCommand.StepOut)
|| cmd.equals(DBGpCommand.stepOver)) {
// Debug target might be already disconnected
if (debugTarget != null) {
debugTarget.suspended(DebugEvent.STEP_END);
}
} else {
/*
* we got another status response, probably due to
* cannot get property error.
*/
}
}
}
}
}
private static enum ResponseHandlerAction {
HANDLE_BREAK, HANDLE_STOPPING, HANDLE_STOP, HANDLE_STREAM;
}
public static final String DEFAULT_SESSION_ENCODING = "ISO-8859-1"; //$NON-NLS-1$
public static final String DEFAULT_BINARY_ENCODING = Charset.defaultCharset().name();
public static final String DEFAULT_OUTPUT_ENCODING = Charset.defaultCharset().name();
private final ISchedulingRule schedulingRule = new ISchedulingRule() {
@Override
public boolean contains(ISchedulingRule rule) {
return rule == this;
}
@Override
public boolean isConflicting(ISchedulingRule rule) {
return rule == this;
}
};
private Socket DBGpSocket;
private ResponseReader responseHandler;
private DBGpCommand DBGpCmd;
private DataInputStream DBGpReader;
private boolean sessionActive = false;
private DBGpTarget debugTarget;
private Hashtable<Integer, Object> savedResponses = new Hashtable<Integer, Object>();
private String ideKey;
private String sessionId;
private String initialScript;
private EngineTypes engineType;
private String engineVersion;
private String threadId;
private long creationTime;
private String sessionEncoding;
private String outputEncoding;
private String binaryEncoding;
public long getCreationTime() {
return creationTime;
}
/**
* create a DBGpSession. This waits for the initial INIT response to be sent
*
* @param connection
* the socket connection.
*/
public DBGpSession(Socket connection) {
creationTime = System.currentTimeMillis();
DBGpSocket = connection;
sessionEncoding = DEFAULT_SESSION_ENCODING;
boolean isGood = false;
try {
DBGpCmd = new DBGpCommand(DBGpSocket);
DBGpReader = new DataInputStream(DBGpSocket.getInputStream());
sessionActive = true;
// TODO: Could look at supporting a timeout here.
byte[] response = readResponse();
if (response != null) {
DBGpResponse parsedResponse = new DBGpResponse();
parsedResponse.parseResponse(response);
if (DBGpResponse.INIT == parsedResponse.getType()) {
ideKey = parsedResponse.getIdekey();
sessionId = parsedResponse.getSession();
initialScript = DBGpUtils.getFilenameFromURIString(parsedResponse.getFileUri());
engineVersion = parsedResponse.getEngineVersion();
engineType = parsedResponse.getEngineType();
threadId = parsedResponse.getThreadId();
isGood = true;
} else {
DBGpLogger.logError("Init response not received. XML=" //$NON-NLS-1$
+ parsedResponse.getRawXML(), this, null);
// TODO: dialog box up
}
} else {
DBGpLogger.logError("Unexpected null from readResponse waiting for Init", //$NON-NLS-1$
this, null);
}
if (!isGood) {
endSession();
}
} catch (UnsupportedEncodingException e) {
DBGpLogger.logException("UnsupportedEncodingException - 1", this, e); //$NON-NLS-1$
endSession();
} catch (IOException e) {
DBGpLogger.logException("IOException - 1", this, e); //$NON-NLS-1$
endSession();
}
}
/**
* Start the session. This schedules the job that listens for incoming
* responses from the system being debugged as these can happen
* asynchronously.
*
*/
public void startSession() {
responseHandler = new ResponseReader();
responseHandler.schedule();
}
/**
* send an async command
*
* @param cmd
* the command to send
*/
public void sendAsyncCmd(String cmd) {
sendAsyncCmd(cmd, null);
}
/**
* send a sync command
*
* @param cmd
* the command to send
* @return the response.
*/
public DBGpResponse sendSyncCmd(String cmd) {
return sendSyncCmd(cmd, null);
}
/**
* send an async command with arguments
*
* @param cmd
* the command
* @param arguments
* its arguments
*/
public void sendAsyncCmd(String cmd, String arguments) {
if (sessionActive) {
int id = DBGpCommand.getNextId();
try {
DBGpCmd.send(cmd, arguments, id, sessionEncoding);
} catch (IOException e) {
endSession();
}
}
}
/**
* send a sync command with arguments
*
* @param cmd
* the command
* @param arguments
* its arguments
* @return the response
*/
public DBGpResponse sendSyncCmd(String cmd, String arguments) {
if (sessionActive) {
/*
* this must be done before the command is sent because the
* savedResponses must have the id and event in the table so that
* the response handler can locate it.
*/
int id = DBGpCommand.getNextId();
Event idev = new Event();
Integer idObj = Integer.valueOf(id);
savedResponses.put(idObj, idev);
try {
DBGpCmd.send(cmd, arguments, id, sessionEncoding);
idev.waitForEvent(); // wait forever
return (DBGpResponse) savedResponses.remove(idObj);
} catch (InterruptedException e) {
return null;
} catch (IOException e) {
endSession();
return null;
}
}
return null;
}
/**
* end this session
*
*/
public synchronized void endSession() {
/*
* We are ending the session so ensure anything that is waiting for a
* response is unblocked.
*/
unblockAllCallers(null);
if (sessionActive) {
sessionActive = false;
try {
DBGpSocket.shutdownInput();
} catch (IOException e) {
}
try {
DBGpSocket.shutdownOutput();
} catch (IOException e) {
}
try {
DBGpSocket.close();
} catch (IOException e) {
// Ignore the exception except for debug purposes
DBGpLogger.debugException(e);
}
}
if (debugTarget != null) {
debugTarget.sessionEnded();
debugTarget = null;
}
}
/**
* get the ide key for this session
*
* @return
*/
public String getIdeKey() {
return ideKey;
}
/**
* get the session id for this session
*
* @return
*/
public String getSessionId() {
return sessionId;
}
/**
* get the Thread id for this session. A blank threadid usually indicates
* that no thread id was returned.
*
* @return the thread id
*/
public String getThreadId() {
return threadId;
}
public boolean isActive() {
return sessionActive;
}
/**
* the initial script is not remapped. Callers of this must remap it itself.
*
* @return
*/
public String getInitialScript() {
return initialScript;
}
public DBGpTarget getDebugTarget() {
return debugTarget;
}
/**
* debug target calls this to say it is owner of the session.
*
* @param debugTarget
*/
public void setDebugTarget(DBGpTarget debugTarget) {
this.debugTarget = debugTarget;
// Now we have a target we can determine the user defined encodings
determineEncodings();
}
/**
* get the current session encoding
*
* @return the session encoding.
*/
public String getSessionEncoding() {
return sessionEncoding;
}
public String getOutputEncoding() {
return outputEncoding;
}
public String getBinaryEncoding() {
return binaryEncoding;
}
/**
* set the session encoding. DBGpTarget determines this when handshaking.
*
* @param sessionEncoding
* session encoding.
*/
public void setSessionEncoding(String sessionEncoding) {
this.sessionEncoding = sessionEncoding;
}
public EngineTypes getEngineType() {
return engineType;
}
public String getEngineVersion() {
return engineVersion;
}
public int getRemotePort() {
return DBGpSocket.getPort();
}
public InetAddress getRemoteAddress() {
return DBGpSocket.getInetAddress();
}
public String getRemoteHostname() {
return DBGpSocket.getInetAddress().getHostName();
}
public String toString() {
StringBuilder strBuf = new StringBuilder(getIdeKey());
if (getSessionId() != null) {
strBuf.append(" - Session:"); //$NON-NLS-1$
strBuf.append(getSessionId());
} else {
strBuf.append(" - Web Server Session"); //$NON-NLS-1$
}
return strBuf.toString();
}
private void unblockAllCallers(DBGpResponse parsedResponse) {
if (parsedResponse == null) {
// if null passed in, create a dummy response.
parsedResponse = new DBGpResponse();
parsedResponse.parseResponse(null);
}
Set<Integer> keys = savedResponses.keySet();
for (Iterator<Integer> iterator = keys.iterator(); iterator.hasNext();) {
Integer idObj = (Integer) iterator.next();
postAndSignalCaller(idObj, parsedResponse);
}
}
private void postAndSignalCaller(Integer idObj, DBGpResponse parsedResponse) {
Object responder = savedResponses.get(idObj);
if (responder instanceof Event) {
/*
* We have an event for the id so we need to respond and unblock the
* caller, otherwise it has already been done (maybe from
* unblockAllCallers)
*/
Event idev = (Event) responder;
savedResponses.put(idObj, parsedResponse);
idev.signalEvent();
}
}
/**
* DBGp protocol is as follows "xxx\0" where xxx is the length of the
* message to follow "message\0" where message is the data we are interested
* in.
*
* @return
*/
private byte[] readResponse() {
byte byteArray[];
byte receivedByte;
int remainingBytesToRead = 0;
try {
/*
* The first part of the DBGp protocol is the length as a string, so
* we read it and convert it to an int
*/
while ((receivedByte = DBGpReader.readByte()) != 0) {
remainingBytesToRead = remainingBytesToRead * 10 + receivedByte - 48;
}
byteArray = new byte[remainingBytesToRead];
int totalBytesSoFar = 0;
while ((remainingBytesToRead > 0)) {
int bytesReceived = DBGpReader.read(byteArray, totalBytesSoFar, remainingBytesToRead);
remainingBytesToRead -= bytesReceived;
totalBytesSoFar += bytesReceived;
}
// Final part of the protocol is a null value
if ((DBGpReader.readByte()) != 0) {
/*
* Unexpected message so the message is not valid, end the
* session as things could become very confused.
*/
endSession();
return null;
}
} catch (IOException e) {
/*
* The exception could be caused by the user terminating or
* disconnecting however due to the nature of the debug framework, a
* termination request may not be sent to the debug target or may be
* sent after it terminates the process, so we cannot rely on
* testing the debug target for it's state to determine if there has
* been any user activity that may have caused this. we could have
* tested and even check the type of exception but on windows you
* get SocketException: Connection Reset and on Linux you get
* EOFException, so for other platforms you don't know what to
* expect as an exception. So it is better to ignore the
* information.
*/
endSession();
return null;
}
try {
if (DBGpLogger.debugResp()) {
DBGpLogger.debug("Response: " //$NON-NLS-1$
+ new String(byteArray, sessionEncoding));
}
return byteArray;
} catch (UnsupportedEncodingException e) {
DBGpLogger.logException("UnsupportedEncodingException - 2", this, e); //$NON-NLS-1$
endSession();
}
return null;
}
private void determineEncodings() {
ILaunch launch = getDebugTarget().getLaunch();
ILaunchConfiguration launchConfig = launch.getLaunchConfiguration();
outputEncoding = getCharset(IDebugParametersKeys.OUTPUT_ENCODING, launchConfig);
binaryEncoding = getCharset(IDebugParametersKeys.TRANSFER_ENCODING, launchConfig);
}
private String getCharset(String encodingKey, ILaunchConfiguration launchConfig) {
String charset = null;
String outputEncoding = null;
if (launchConfig != null) {
try {
outputEncoding = launchConfig.getAttribute(encodingKey, ""); //$NON-NLS-1$
} catch (CoreException e) {
}
}
if (outputEncoding == null || outputEncoding.length() == 0) {
// null will return it from the main preferences
if (encodingKey == IDebugParametersKeys.OUTPUT_ENCODING) {
outputEncoding = PHPProjectPreferences.getOutputEncoding(null);
} else {
outputEncoding = PHPProjectPreferences.getTransferEncoding(null);
}
}
if (outputEncoding == null || Charset.isSupported(outputEncoding) == false) {
charset = Charset.defaultCharset().name();
} else {
charset = outputEncoding;
}
return charset;
}
}