/*******************************************************************************
* 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.zend.communication;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.*;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.php.debug.core.debugger.handlers.IDebugMessageHandler;
import org.eclipse.php.debug.core.debugger.handlers.IDebugRequestHandler;
import org.eclipse.php.debug.core.debugger.messages.IDebugMessage;
import org.eclipse.php.debug.core.debugger.messages.IDebugNotificationMessage;
import org.eclipse.php.debug.core.debugger.messages.IDebugRequestMessage;
import org.eclipse.php.debug.core.debugger.messages.IDebugResponseMessage;
import org.eclipse.php.debug.core.debugger.parameters.IDebugParametersKeys;
import org.eclipse.php.internal.core.util.BlockingQueue;
import org.eclipse.php.internal.core.util.collections.IntHashtable;
import org.eclipse.php.internal.debug.core.*;
import org.eclipse.php.internal.debug.core.launching.DebugSessionIdGenerator;
import org.eclipse.php.internal.debug.core.launching.PHPLaunchUtilities;
import org.eclipse.php.internal.debug.core.launching.PHPProcess;
import org.eclipse.php.internal.debug.core.preferences.PHPDebugCorePreferenceNames;
import org.eclipse.php.internal.debug.core.preferences.PHPProjectPreferences;
import org.eclipse.php.internal.debug.core.zend.debugger.*;
import org.eclipse.php.internal.debug.core.zend.debugger.handlers.FileContentDebugHandler;
import org.eclipse.php.internal.debug.core.zend.debugger.messages.*;
import org.eclipse.php.internal.debug.core.zend.debugger.parameters.AbstractDebugParametersInitializer;
import org.eclipse.php.internal.debug.core.zend.debugger.parameters.DefaultDebugParametersInitializer;
import org.eclipse.php.internal.debug.core.zend.model.PHPDebugTarget;
import org.eclipse.php.internal.debug.core.zend.model.PHPMultiDebugTarget;
import org.eclipse.php.internal.debug.core.zend.testConnection.DebugServerTestController;
import org.eclipse.php.internal.debug.core.zend.testConnection.DebugServerTestEvent;
import org.eclipse.php.internal.server.core.Server;
import org.eclipse.swt.widgets.Display;
import com.ibm.icu.text.MessageFormat;
/**
* The debug connection is responsible of initializing and handle a single debug
* session that was triggered by a remote or local debugger.
*
* @author shalom
*/
public class DebugConnection {
// Launch configuration type for handling an external launch triggers.
private static final String SERVER_DEBUG_NAME = "PHP Debug"; //$NON-NLS-1$
private static final String SERVER_PROFILE_NAME = "PHP Profile"; //$NON-NLS-1$
/**
* This job handles the requests and notification that are inserted into the
* queues by the MessageReceiver job.
*/
private class MessageHandler extends Job {
private BlockingQueue inputMessageQueue = new BlockingQueue(100);
private ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
private DataOutputStream outArray = new DataOutputStream(byteArray);
public MessageHandler() {
super("Debug Message Handler"); //$NON-NLS-1$
setSystem(true);
setUser(false);
setPriority(LONG);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.
* IProgressMonitor)
*/
public IStatus run(IProgressMonitor monitor) {
// 'while(true)' is OK here since we have blocking queue
while (true) {
if (monitor.isCanceled())
return Status.OK_STATUS;
try {
IDebugMessage incomingMessage = (IDebugMessage) inputMessageQueue.queueOut();
Logger.debugMSG("NEW MESSAGE RECEIVED: " + incomingMessage); //$NON-NLS-1$
try {
boolean isDebugConnectionTest = false;
// First debug message has arrived (SESSION STARTED)
if (incomingMessage instanceof DebugSessionStartedNotification) {
DebugSessionStartedNotification sessionStartedMessage = (DebugSessionStartedNotification) incomingMessage;
setResponseTimeout(sessionStartedMessage);
isDebugConnectionTest = sessionStartedMessage.getQuery()
.indexOf("testConnection=true") != -1; //$NON-NLS-1$
// This is a connection test...
if (isDebugConnectionTest) {
String sourceHost = DebugConnection.this.socket.getInetAddress().getHostAddress();
// Notify success
if (verifyProtocolID(sessionStartedMessage.getServerProtocolID())) {
sendRequest(new StartRequest());
DebugServerTestController.getInstance().notifyTestListener(
new DebugServerTestEvent(sourceHost, DebugServerTestEvent.TEST_SUCCEEDED));
} else {
DebugServerTestController.getInstance().notifyTestListener(new DebugServerTestEvent(
sourceHost, DebugServerTestEvent.TEST_FAILED_DEBUGER_VERSION));
}
}
// START DEBUG (create debug target)
else {
hookDebugSession(sessionStartedMessage);
}
}
// Creation of debug session has succeeded
if (debugTarget != null) {
// Try to find relevant handler for the message:
IDebugMessageHandler messageHandler = createMessageHandler(incomingMessage);
if (messageHandler != null) {
Logger.debugMSG("CREATING MESSAGE HANDLER: " //$NON-NLS-1$
+ messageHandler.getClass().getName().replaceFirst(".*\\.", "")); //$NON-NLS-1$ //$NON-NLS-2$
// Handle the request
messageHandler.handle(incomingMessage, debugTarget);
if (messageHandler instanceof IDebugRequestHandler) {
// Create response
IDebugResponseMessage response = ((IDebugRequestHandler) messageHandler)
.getResponseMessage();
// Send response
synchronized (connectionOut) {
byteArray.reset();
response.serialize(outArray);
connectionOut.writeInt(byteArray.size());
byteArray.writeTo(connectionOut);
connectionOut.flush();
}
}
}
// Handle the response
else if (incomingMessage instanceof IDebugResponseMessage) {
IDebugResponseMessage r = (IDebugResponseMessage) incomingMessage;
// Take the request ID from the response.
int requestId = r.getID();
// Find the request.
IDebugRequestMessage req = (IDebugRequestMessage) requestsTable.remove(requestId);
// Find the handler.
ResponseHandler handler = responseHandlers.remove(Integer.valueOf(requestId));
handler.handleResponse(req, r);
}
// Handle dummy connection close
else if (incomingMessage == CONNECTION_CLOSED) {
handleClosed();
}
}
// No debug target?
else {
handleClosed();
}
}
// Error processing the current message.
catch (Exception e) {
PHPDebugPlugin.log(e);
}
} catch (Exception e) {
PHPDebugPlugin.log(e);
shutdown();
}
}
}
public void queueIn(IDebugMessage m) {
inputMessageQueue.queueIn(m);
}
void shutdown() {
cancel();
inputMessageQueue.releaseReaders();
inputMessageQueue.clear();
}
/**
* This method is called by the message receiver so that the message
* handler will queueIn an internal protocol message for the closure of
* connection.
*/
void connectionClosed() {
queueIn(CONNECTION_CLOSED);
}
private void handleClosed() {
if (getCommunicationAdministrator() != null) {
getCommunicationAdministrator().connectionClosed();
}
shutdown();
// Have to be here as well as in message receiver
DebugConnection.this.terminate();
}
}
/**
* This job manages the communication initiated by the peer. All the
* messages that arrive from the peer are read by the MessageReceiver that
* will then handle the message by the message type.
*/
private class MessageReceiver extends Job {
private String transferEncoding;
private String outputEncoding;
/**
* Create an InputManager in a separate thread.
*/
public MessageReceiver() {
super("Debug Message Receiver"); //$NON-NLS-1$
setSystem(true);
setUser(false);
setPriority(LONG);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.
* IProgressMonitor)
*/
public IStatus run(IProgressMonitor monitor) {
// 'while(true)' is OK here since we use blocking read
while (true) {
try {
if (monitor.isCanceled())
return Status.OK_STATUS;
// Reads the length
int length = connectionIn.readInt();
if (length < 0) {
String message = "Socket error (length is negative): possibly Server is SSL, Client is not."; //$NON-NLS-1$
Logger.debugMSG(message);
Logger.log(Logger.ERROR, message);
shutdown();
continue;
}
// We have a new message. process it !!.
int messageType = connectionIn.readShort();
/*
* If this is the first message, the protocol is still held
* as invalid. Check that the first message has the
* DebugSessionStartedNotification type. If not, then we can
* assume that the remote debugger protocol has a different
* version then expected.
*/
if (!isValidProtocol && messageType != START_MESSAGE_ID) {
showProtocolError();
shutdown();
continue;
}
isValidProtocol = true;
// Create message with the use of registry
IDebugMessage message = DebugMessagesRegistry.getMessage(messageType);
if (message != null) {
if (message instanceof OutputNotification) {
message.setTransferEncoding(outputEncoding);
} else {
message.setTransferEncoding(transferEncoding);
}
}
// Handle the incoming message
if (message instanceof IDebugNotificationMessage) {
message.deserialize(connectionIn);
// PUT NOTIFICATION TO NOTIFICATION QUEUE
messageHandler.queueIn(message);
} else if (message instanceof IDebugResponseMessage) {
message.deserialize(connectionIn);
int messageId = ((IDebugResponseMessage) message).getID();
/*
* INSERT RESPONSE TO TABLE AND RELEASE THE THREAD
* WAITING FOR THE REQUEST
*/
// Find the handler.
ResponseHandler handler = responseHandlers.get(Integer.valueOf(messageId));
if (handler == null) {
responseTable.put(/* requestId */messageId, message);
// Find the request.
IDebugRequestMessage request = (IDebugRequestMessage) requestsTable.remove(messageId);
if (request != null) {
// Notify the RESPONSE is here.
synchronized (request) {
request.notifyAll();
}
} else {
// Remove this message.
responseTable.remove(messageId);
}
} else {
messageHandler.queueIn(message);
}
}
// This is a request.
else if (message instanceof IDebugRequestMessage) {
message.deserialize(connectionIn);
messageHandler.queueIn(message);
}
} catch (IOException e) {
// Probably, the connection was dumped
shutdown();
continue;
} catch (Exception e) {
PHPDebugPlugin.log(e);
shutdown();
}
}
}
/**
* Sets the transfer encoding.
*
* @param transferEncoding
*/
public void setTransferEncoding(String transferEncoding) {
this.transferEncoding = transferEncoding;
}
/**
* Set the debug output encoding. The output encoding effects the
* {@link OutputNotification} strings encoding.
*
* @param outputEncoding
*/
public void setOutputEncoding(String outputEncoding) {
this.outputEncoding = outputEncoding;
}
void shutdown() {
cancel();
// Shutdown message handler
messageHandler.connectionClosed();
// Have to be here as well as in message handler
DebugConnection.this.terminate();
}
private void showProtocolError() {
final String errorMessage = MessageFormat.format(PHPDebugCoreMessages.Debugger_Incompatible_Protocol,
new Object[] { String.valueOf(RemoteDebugger.PROTOCOL_ID_LATEST) });
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(), "Debugger Error", errorMessage); //$NON-NLS-1$
}
});
}
}
protected class SessionDescriptor {
private int id;
private int ordinal;
private DebugSessionStartedNotification startedNotification;
public SessionDescriptor(DebugSessionStartedNotification startedNotification) {
this.startedNotification = startedNotification;
this.id = -1;
this.ordinal = -1;
build();
}
public DebugSessionStartedNotification getStartedNotification() {
return startedNotification;
}
private void build() {
String params;
if (startedNotification.getQuery().contains(AbstractDebugParametersInitializer.DEBUG_SESSION_ID))
params = startedNotification.getQuery();
else
params = startedNotification.getOptions();
List<String> parameters = Arrays.asList(params.split("&")); //$NON-NLS-1$
Iterator<String> i = parameters.iterator();
while (i.hasNext()) {
String parameter = i.next();
if (parameter.startsWith(AbstractDebugParametersInitializer.DEBUG_SESSION_ID)) {
int idx = parameter.indexOf('=');
Integer parsedId = parseInt(parameter.substring(idx + 1));
if (parsedId != null)
id = parsedId;
if (i.hasNext()) {
Integer parsedOrdinal = parseInt(i.next());
if (parsedOrdinal != null)
ordinal = parsedOrdinal;
}
}
}
}
private Integer parseInt(String number) {
try {
return Integer.parseInt(number);
} catch (NumberFormatException e) {
return null;
}
}
public int getId() {
return id;
}
public int getOrdinal() {
return ordinal;
}
public boolean isPrimary() {
return getOrdinal() < 0;
}
public boolean isUnknown() {
return getId() < 0 && getOrdinal() < 0;
}
}
// Launch configuration type for handling an external launch triggers.
public static final String REMOTE_LAUNCH_TYPE_ID = "org.eclipse.php.debug.core.remotePHPLaunchConfigurationType"; //$NON-NLS-1$
public static final String REMOTE_DEBUG_LAUNCH_NAME = "PHP Debug"; //$NON-NLS-1$
private static final Lock HOOK_LOCK = new ReentrantLock(true);
private static final long HOOK_TIMEOUT = 10000;
// Phantom message used to notify that connection was closed
private final IDebugMessage CONNECTION_CLOSED = new DebugMessageImpl() {
public void deserialize(DataInputStream in) throws IOException {
}
public int getType() {
return 0;
}
public void serialize(DataOutputStream out) throws IOException {
}
};
protected static final int START_MESSAGE_ID = (new DebugSessionStartedNotification()).getType();
protected int debugResponseTimeout;
protected PHPDebugTarget debugTarget;
protected boolean isValidProtocol;
private Socket socket;
private DataInputStream connectionIn;
private DataOutputStream connectionOut;
private boolean isInitialized;
private MessageReceiver messageReceiver;
private MessageHandler messageHandler;
private CommunicationClient communicationClient;
private CommunicationAdministrator communicationAdministrator;
private IntHashtable requestsTable;
private IntHashtable responseTable;
private Hashtable<Integer, ResponseHandler> responseHandlers;
private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
private DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
private int lastRequestID = 1000;
private Map<Integer, IDebugMessageHandler> messageHandlers;
private boolean isConnected = true;
/**
* Constructs a new DebugConnectionThread with a given Socket.
*
* @param socket
*/
public DebugConnection(Socket socket) {
this.socket = socket;
connect();
}
/**
* Closes the connection. Causes message receiver & handler to be shutdown.
*/
public synchronized void disconnect() {
messageReceiver.shutdown();
}
/**
* Returns true if the connection is alive.
*/
public boolean isConnected() {
return isConnected;
}
/**
* Returns communication client
*
* @return communication client
*/
public CommunicationClient getCommunicationClient() {
return communicationClient;
}
/**
* Returns communication administrator
*
* @return communication administrator
*/
public CommunicationAdministrator getCommunicationAdministrator() {
return communicationAdministrator;
}
/**
* Sends a notification.
*
* @param msg
* The delivered notification message.
*/
public void sendNotification(Object msg) {
if (!isConnected)
// Skip if already disconnected
return;
try {
synchronized (connectionOut) {
byteArrayOutputStream.reset();
((IDebugMessage) msg).serialize(dataOutputStream);
connectionOut.writeInt(byteArrayOutputStream.size());
byteArrayOutputStream.writeTo(connectionOut);
connectionOut.flush();
}
} catch (SocketException se) {
// Probably because the remote host disconnected.
// Just log a warning (might be removed in the near future).
if (PHPDebugPlugin.DEBUG) {
Logger.log(Logger.WARNING, se.getMessage(), se);
}
} catch (Exception exc) {
PHPDebugPlugin.log(exc);
}
}
/**
* Send a synchronous request & wait for a response
*
* @param request
* The delivered Request message.
* @return A response for the delivered request.
*/
public Object sendRequest(Object request) throws Exception {
if (!isConnected)
// Skip if already disconnected
return null;
Logger.debugMSG("SENDING SYNCHRONOUS REQUEST: " + request); //$NON-NLS-1$
try {
IDebugRequestMessage theMsg = (IDebugRequestMessage) request;
synchronized (connectionOut) {
byteArrayOutputStream.reset();
theMsg.setID(lastRequestID++);
theMsg.serialize(dataOutputStream);
int messageSize = byteArrayOutputStream.size();
requestsTable.put(theMsg.getID(), theMsg);
connectionOut.writeInt(messageSize);
byteArrayOutputStream.writeTo(connectionOut);
connectionOut.flush();
}
IDebugResponseMessage response = null;
int timeoutTick = 500; // 0.5 of second
int waitedTime = 0;
while (response == null && isConnected()) {
synchronized (request) {
response = (IDebugResponseMessage) responseTable.remove(theMsg.getID());
if (response == null) {
/*
* Display a progress dialog after a quarter of the
* assigned time have passed.
*/
if (waitedTime > debugResponseTimeout / 4) {
/*
* Display a message that we are waiting for the
* server response. In case that the response
* finally arrives, remove the message. In case we
* have a timeout, close the connection and display
* a different message.
*/
PHPLaunchUtilities.showWaitForDebuggerMessage(this);
}
// Wait for notify from MessageReceiver
request.wait(timeoutTick);
}
}
if (response == null) {
response = (IDebugResponseMessage) responseTable.remove(theMsg.getID());
}
/*
* if the response is null. it means that there is no answer
* from the server. This can be because on the
* peerResponseTimeout.
*/
if (response == null && isConnected()) {
Logger.debugMSG("COMMUNICATION PROBLEMS (response is null)"); //$NON-NLS-1$
// Handle time out will stop the communication if needed.
if (waitedTime < debugResponseTimeout - timeoutTick) {
waitedTime += timeoutTick;
handlePeerResponseTimeout();
} else {
disconnect();
PHPLaunchUtilities.hideWaitForDebuggerMessage();
PHPLaunchUtilities.showLaunchErrorMessage();
}
if (!isConnected())
break;
}
}
PHPLaunchUtilities.hideWaitForDebuggerMessage();
Logger.debugMSG("RECEIVED RESPONSE: " + response); //$NON-NLS-1$
return response;
} catch (IOException e) { // Return null for any exception
PHPDebugPlugin.log(e);
} catch (InterruptedException e) {// Return null for any exception
PHPDebugPlugin.log(e);
}
return null;
}
/**
* Send an asynchronous request.
*
* @param request
* @param responseHandler
*/
public void sendRequest(Object request, ResponseHandler responseHandler) {
if (!isConnected)
// Skip if already disconnected
return;
Logger.debugMSG("SENDING ASYNCHRONOUS REQUEST: " + request); //$NON-NLS-1$
int msgId = lastRequestID++;
IDebugRequestMessage theMsg = (IDebugRequestMessage) request;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
theMsg.setID(msgId);
theMsg.serialize(dataOutputStream);
int messageSize = byteArrayOutputStream.size();
synchronized (connectionOut) {
requestsTable.put(msgId, request);
responseHandlers.put(Integer.valueOf(msgId), responseHandler);
connectionOut.writeInt(messageSize);
byteArrayOutputStream.writeTo(connectionOut);
connectionOut.flush();
}
} catch (Exception e) {
// Return null for any exception
String message = "Exception for request NO." + theMsg.getType() //$NON-NLS-1$
+ e.toString();
Logger.debugMSG(message);
Logger.logException(e);
responseHandler.handleResponse(request, null);
responseHandlers.remove(Integer.valueOf(msgId));
}
}
/**
* Sets communication administrator.
*
* @param admin
*/
public void setCommunicationAdministrator(CommunicationAdministrator admin) {
communicationAdministrator = admin;
}
/**
* Sets communication client.
*
* @param client
*/
public void setCommunicationClient(CommunicationClient client) {
this.communicationClient = client;
}
protected boolean hookLaunch(SessionDescriptor sessionDescriptor) throws CoreException {
// Try to hook any of the existing launches
ILaunch launch = PHPSessionLaunchMapper.get(sessionDescriptor.getId());
if (launch != null && isDifferentOriginalURL(sessionDescriptor, launch))
return true;
/*
* There are no existing launches (created by user nor "mock" ones) for
* incoming session ID, try to find/create new "mock" launch
*/
if (launch == null)
launch = fetchLaunch(sessionDescriptor);
else if (isBuildinServerLaunch(launch)) {
hookBuiltinServerLaunch(launch, sessionDescriptor);
return true;
/*
* If session is primary (new one has come) and launch exists then
* it means that session has been restarted. If so, terminate the
* previous launch.
*/
} else if (sessionDescriptor.isPrimary() && !launch.isTerminated())
try {
launch.terminate();
} catch (DebugException e) {
// ignore
}
// Move on with the launch
if (launch != null) {
// Remove terminated elements if any
cleanup(launch);
// Hook by launch type
if (isServerLaunch(launch)) {
hookServerLaunch(launch, sessionDescriptor);
} else {
hookPHPExeLaunch(launch, sessionDescriptor);
}
return true;
}
return false;
}
/**
* Hook a server debug session
*
* @param launch
* An {@link ILaunch}
* @param startedNotification
* A DebugSessionStartedNotification
*/
protected void hookBuiltinServerLaunch(final ILaunch launch, SessionDescriptor sessionDescriptor)
throws CoreException {
String uri = sessionDescriptor.getStartedNotification().getUri();
// for builtin server, the requested uri should be something like
// "/projectName/index.php?debug_params..."
// TODO needs a better way to detect project name
String scriptName = uri.substring(0, uri.indexOf('?'));
String projectName = scriptName.split("/", 3)[1];
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
messageReceiver
.setTransferEncoding(launchConfiguration.getAttribute(IDebugParametersKeys.TRANSFER_ENCODING, "")); //$NON-NLS-1$
messageReceiver.setOutputEncoding(launchConfiguration.getAttribute(IDebugParametersKeys.OUTPUT_ENCODING, "")); //$NON-NLS-1$
String URL = launchConfiguration.getAttribute(Server.BASE_URL, "") + scriptName; //$NON-NLS-1$
int requestPort = PHPDebugPlugin.getDebugPort(DebuggerCommunicationDaemon.ZEND_DEBUGGER_ID);
try {
requestPort = Integer.valueOf(launch.getAttribute(IDebugParametersKeys.PORT));
} catch (Exception e) {
// should not happen
}
boolean runWithDebug = launchConfiguration.getAttribute(IPHPDebugConstants.RUN_WITH_DEBUG_INFO, true);
if (launch.getLaunchMode().equals(ILaunchManager.DEBUG_MODE)) {
runWithDebug = false;
}
debugTarget = new PHPDebugTarget(this, launch, URL, requestPort, launch.getProcesses()[0], runWithDebug, false,
project);
// Bind debug target to the launch
bindTarget(launch);
}
/**
* Hook a server debug session
*
* @param launch
* An {@link ILaunch}
* @param startedNotification
* A DebugSessionStartedNotification
*/
protected void hookServerLaunch(final ILaunch launch, SessionDescriptor sessionDescriptor) throws CoreException {
ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
IProject project = getProject(launchConfiguration);
messageReceiver
.setTransferEncoding(launchConfiguration.getAttribute(IDebugParametersKeys.TRANSFER_ENCODING, "")); //$NON-NLS-1$
messageReceiver.setOutputEncoding(launchConfiguration.getAttribute(IDebugParametersKeys.OUTPUT_ENCODING, "")); //$NON-NLS-1$
String URL = launchConfiguration.getAttribute(Server.BASE_URL, ""); //$NON-NLS-1$
boolean stopAtFirstLine = project == null ? true : PHPProjectPreferences.getStopAtFirstLine(project);
int requestPort = PHPDebugPlugin.getDebugPort(DebuggerCommunicationDaemon.ZEND_DEBUGGER_ID);
try {
requestPort = Integer.valueOf(launch.getAttribute(IDebugParametersKeys.PORT));
} catch (Exception e) {
// should not happen
}
boolean runWithDebug = launchConfiguration.getAttribute(IPHPDebugConstants.RUN_WITH_DEBUG_INFO, true);
if (launch.getLaunchMode().equals(ILaunchManager.DEBUG_MODE)) {
runWithDebug = false;
}
PHPProcess process = new PHPProcess(launch, URL);
debugTarget = (PHPDebugTarget) createDebugTarget(this, launch, URL, requestPort, process, runWithDebug,
stopAtFirstLine, project);
// Bind debug target to the launch
bindTarget(launch);
}
/**
* Hook a PHP executable debug session
*
* @param launch
* An {@link ILaunch}
* @param startedNotification
* A DebugSessionStartedNotification
*/
protected void hookPHPExeLaunch(final ILaunch launch, SessionDescriptor sessionDescriptor) throws CoreException {
ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
messageReceiver
.setTransferEncoding(launchConfiguration.getAttribute(IDebugParametersKeys.TRANSFER_ENCODING, "")); //$NON-NLS-1$
messageReceiver.setOutputEncoding(launchConfiguration.getAttribute(IDebugParametersKeys.OUTPUT_ENCODING, "")); //$NON-NLS-1$
String phpExeString = launchConfiguration.getAttribute(IPHPDebugConstants.ATTR_EXECUTABLE_LOCATION,
(String) null);
String fileNameString = launchConfiguration.getAttribute(IPHPDebugConstants.ATTR_FILE, (String) null);
boolean runWithDebugInfo = launchConfiguration.getAttribute(IPHPDebugConstants.RUN_WITH_DEBUG_INFO, true);
IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
IProject project = null;
String file = launchConfiguration.getAttribute(IPHPDebugConstants.ATTR_FILE, (String) null);
if (file != null) {
IResource resource = workspaceRoot.findMember(file);
if (resource != null) {
project = resource.getProject();
}
}
if (launch.getLaunchMode().equals(ILaunchManager.DEBUG_MODE)) {
runWithDebugInfo = false;
}
String debugFileName = fileNameString;
IPath filePath = new Path(fileNameString);
IResource res = workspaceRoot.findMember(filePath);
if (res != null) {
IFile fileToDebug = (IFile) res;
debugFileName = fileToDebug.getName();
}
boolean stopAtFirstLine = PHPProjectPreferences.getStopAtFirstLine(project);
int requestPort = PHPDebugPlugin.getDebugPort(DebuggerCommunicationDaemon.ZEND_DEBUGGER_ID);
try {
requestPort = Integer.valueOf(launch.getAttribute(IDebugParametersKeys.PORT));
} catch (Exception e) {
// should not happen
}
IPath phpExe = new Path(phpExeString);
PHPProcess process = new PHPProcess(launch, phpExe.toOSString());
debugTarget = (PHPDebugTarget) createDebugTarget(this, launch, phpExeString, debugFileName, requestPort,
process, runWithDebugInfo, stopAtFirstLine, project);
// Bind debug target to the launch
bindTarget(launch);
}
/**
* Hook a session that should handle a file content request only when the
* Zend Platform GUI is asking to display the source code of the script that
* caused an event.
*
* @param fileName
* The requested file.
* @param lineNumber
* Line number
*/
protected void hookFileContentSession(DebugSessionStartedNotification notification, String fileName,
int lineNumber) {
try {
FileContentDebugHandler handler = new FileContentDebugHandler(this);
IRemoteDebugger debugger = handler.getRemoteDebugger();
if (!debugger.isActive()) {
throw new IllegalStateException(
"Could not read the content of the file. The debugger is not connected."); //$NON-NLS-1$
}
try {
if (PHPDebugUtil.isSystem5()) {
debugger.setProtocol(RemoteDebugger.COMMERCIAL_I5_PROTOCOL_ID_LATEST);
} else {
debugger.setProtocol(RemoteDebugger.COMMERCIAL_PROTOCOL_ID_LATEST);
}
try {
fileName = URLDecoder.decode(fileName, "UTF-8"); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
}
byte[] content = debugger.getFileContent(fileName);
if (content == null) {
content = new byte[0];
}
String serverAddress = extractParameterFromQuery(notification.getOptions(), "server_address");
String originalURL = notification.getUri();
IRemoteFileContentRequestor requestor = RemoteFileContentRequestorsRegistry.getInstance()
.removeRequestor(fileName, lineNumber);
if (requestor == null) {
RemoteFileContentRequestorsRegistry.getInstance().handleExternalRequest(content, serverAddress,
originalURL, fileName, lineNumber);
return;
}
requestor.fileContentReceived(content, serverAddress, originalURL, fileName, lineNumber);
} finally {
debugger.closeDebugSession();
}
} catch (Exception e) {
Logger.logException(e);
}
}
/**
* Handle a debug session hook error. This method can be subclassed for
* handling more complex causes. The default implementation is to display
* the toString() value of the cause and return false.
*
* @param cause
* An object that represents the cause for the error. Can be a
* String description or a different complex object that can
* supply more information.
* @return True, if the error was fixed in this method; False, otherwise.
*/
protected void hookError(Object cause) {
// Nothing to hook yet
}
protected ILaunch fetchLaunch(SessionDescriptor sessionDescriptor) throws CoreException {
final String query = sessionDescriptor.getStartedNotification().getQuery();
// Check for a file content request session.
String fileContentRequestFile = getFileContentRequestPath(query);
if (fileContentRequestFile != null) {
hookFileContentSession(sessionDescriptor.getStartedNotification(), fileContentRequestFile,
getLineNumber(query));
return null;
}
// Find out if the session is for profile.
boolean isProfile = false;
String additionalOptions = sessionDescriptor.getStartedNotification().getOptions();
if (additionalOptions != null && additionalOptions.indexOf("start_profile=1") > -1) { //$NON-NLS-1$
isProfile = true;
}
/*
* The super implementation failed to hook the session to any existing
* launch, or the session is a profile session.
*/
boolean isDebugOrProfileURL = isDebugOrProfileURL(query);
int sessionID = sessionDescriptor.getId();
if (!isDebugOrProfileURL) {
/*
* First, find out if the session ID is not an older one that was
* sent because the browser cached a cookie which is no longer valid
* for us.
*/
if (sessionID > 0 && sessionID <= DebugSessionIdGenerator.getLastGenerated()) {
if (PHPDebugPlugin.DEBUG) {
PHPDebugPlugin.logErrorMessage(
"Terminating a requested session.\nThe session id received is lower than the last generated."); //$NON-NLS-1$
}
return null;
}
}
/*
* In this case we can assume that the launch is similar to a web server
* debug or profile session.
*/
ILaunchConfigurationType lcType = DebugPlugin.getDefault().getLaunchManager()
.getLaunchConfigurationType(REMOTE_LAUNCH_TYPE_ID);
/*
* Prepare to use the debug or profile perspective in case it's not
* installed yet (first time of using the Debug/Profile URL).
*/
if (!isProfile) {
DebugUITools.setLaunchPerspective(lcType, "debug", //$NON-NLS-1$
"org.eclipse.debug.ui.DebugPerspective"); // $NON-NLS-1$
} else {
DebugUITools.setLaunchPerspective(lcType, "profile", //$NON-NLS-1$
"org.eclipse.php.profile.ui.perspective"); // $NON-NLS-1$
}
ILaunchConfigurationWorkingCopy wc = lcType.newInstance(null,
(isProfile) ? SERVER_PROFILE_NAME : SERVER_DEBUG_NAME);
wc.setAttribute(IPHPDebugConstants.RUN_WITH_DEBUG_INFO, true);
if (additionalOptions != null
&& additionalOptions.indexOf(DefaultDebugParametersInitializer.DEBUG_NO_REMOTE + "=1") > -1) { //$NON-NLS-1$
wc.setAttribute(IPHPDebugConstants.DEBUGGING_USE_SERVER_FILES, true);
}
String originalURL = getOriginalURL(additionalOptions);
if (originalURL == null) {
// Use the URI instead
originalURL = sessionDescriptor.getStartedNotification().getUri();
}
wc.setAttribute(IDebugParametersKeys.WEB_SERVER_DEBUGGER, Boolean.toString(true));
wc.setAttribute(Server.BASE_URL, originalURL);
wc.doSave();
ILaunch launch = DebugUITools.buildAndLaunch(wc,
(isProfile) ? ILaunchManager.PROFILE_MODE : ILaunchManager.DEBUG_MODE, new NullProgressMonitor());
/*
* In case we got here, we need to update the PHPSessionLaunchMapper
* with the new launch and the acquired launch id. This is a case when
* we get a launch id from the toolbar or from the Platform.
*/
if (sessionID < 0)
sessionID = DebugSessionIdGenerator.generateSessionID();
PHPSessionLaunchMapper.put(sessionID, launch);
if (PHPDebugPlugin.DEBUG)
System.out.println("Added a remote launch mapping to session with ID: " //$NON-NLS-1$
+ sessionID);
return launch;
}
/**
* Creates a new IDebugTarget. This create method is usually used when
* hooking a PHP web page launch.
*
* @throws CoreException
*/
protected IDebugTarget createDebugTarget(DebugConnection thread, ILaunch launch, String url, int requestPort,
PHPProcess process, boolean runWithDebug, boolean stopAtFirstLine, IProject project) throws CoreException {
return new PHPDebugTarget(thread, launch, url, requestPort, process, runWithDebug, stopAtFirstLine, project);
}
/**
* Creates a new IDebugTarget. This create method is usually used when
* hooking a PHP executable launch.
*
* @throws CoreException
*/
protected IDebugTarget createDebugTarget(DebugConnection thread, ILaunch launch, String phpExeString,
String debugFileName, int requestPort, PHPProcess process, boolean runWithDebugInfo,
boolean stopAtFirstLine, IProject project) throws CoreException {
return new PHPDebugTarget(thread, launch, phpExeString, debugFileName, requestPort, process, runWithDebugInfo,
stopAtFirstLine, project);
}
/**
* Get {@link IProject} instance from provided launch configuration.
*
* @param configuration
* @return {@link IProject}
* @throws CoreException
*/
protected IProject getProject(ILaunchConfiguration configuration) throws CoreException {
String projectName = configuration.getAttribute(IPHPDebugConstants.PHP_Project, (String) null);
if (projectName != null) {
return ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
}
return null;
}
protected boolean setProtocol(int protocolID) {
SetProtocolRequest request = new SetProtocolRequest();
request.setProtocolID(protocolID);
try {
Object response = sendRequest(request);
if (response != null && response instanceof SetProtocolResponse) {
int responceProtocolID = ((SetProtocolResponse) response).getProtocolID();
if (responceProtocolID == protocolID) {
return true;
}
}
} catch (Exception e) {
Logger.logException(e);
}
return false;
}
/**
* This method checks whether the server protocol is older than the latest
* Studio protocol.
*
* @return <code>true</code> if debugger protocol matches the Studio
* protocol, otherwise <code>false</code>
*/
protected boolean verifyProtocolID(int serverProtocolID) {
if (serverProtocolID < RemoteDebugger.PROTOCOL_ID_LATEST) {
return setProtocol(RemoteDebugger.PROTOCOL_ID_LATEST);
}
return true;
}
/**
* This method checks whether this launch is a server one.
*
* @param launch
* @return <code>true</code> if launch is server one, otherwise
* <code>false</code>
*/
protected boolean isServerLaunch(ILaunch launch) {
return Boolean.toString(true).equals(launch.getAttribute(IDebugParametersKeys.WEB_SERVER_DEBUGGER));
}
protected boolean isBuildinServerLaunch(ILaunch launch) {
return Boolean.toString(true).equals(launch.getAttribute(IDebugParametersKeys.BUILTIN_SERVER_DEBUGGER));
}
/**
* Hook the debug session to the correct ILaunch that started it.
*
* @param debugSessionStartedNotification
* @return True, if the debug session hook was successful; False, otherwise.
*/
private void hookDebugSession(DebugSessionStartedNotification debugSessionStartedNotification)
throws CoreException {
/*
* Try to hook (debug session -> launch) only one at a time, just to
* avoid an ugly mess with debug events.
*/
try {
// Do not lock forever
HOOK_LOCK.tryLock(HOOK_TIMEOUT, TimeUnit.MILLISECONDS);
SessionDescriptor sessionDescriptor = new SessionDescriptor(debugSessionStartedNotification);
if (!hookLaunch(sessionDescriptor))
// May happen
hookError("No session id"); //$NON-NLS-1$
} catch (InterruptedException e) {
Logger.logException(e);
} finally {
HOOK_LOCK.unlock();
}
}
/**
* Bind debug target to the launch finally.
*
* @param launch
* @throws CoreException
*/
private void bindTarget(ILaunch launch) throws CoreException {
IDebugTarget target = launch.getDebugTarget();
if (target != null) {
/*
* Launch already has one multiple-threaded target, extend it with
* incoming sub-target.
*/
if (target instanceof PHPMultiDebugTarget) {
PHPMultiDebugTarget multi = (PHPMultiDebugTarget) target;
multi.addSubTarget(debugTarget);
}
/*
* Launch already has one single-threaded target, replace it with
* multiple-threaded one.
*/
else if (target instanceof PHPDebugTarget) {
PHPDebugTarget single = (PHPDebugTarget) target;
// Cleanup 'single' info
launch.removeDebugTarget(single);
IProcess[] processes = launch.getProcesses();
for (IProcess p : processes)
launch.removeProcess(p);
// Create 'multi' process & target
PHPProcess process = new PHPProcess(launch, "Parallel Requests' Process"); //$NON-NLS-1$
PHPMultiDebugTarget multi = new PHPMultiDebugTarget(launch, process);
multi.addSubTarget(single);
multi.addSubTarget(debugTarget);
// Connect to launch
launch.addDebugTarget(multi);
launch.addProcess(process);
}
} else {
// It is just single-threaded target
launch.addDebugTarget(debugTarget);
launch.addProcess(debugTarget.getProcess());
}
}
/**
* Clean up launch. Remove terminated launches and processes.
*
* @param launch
*/
private void cleanup(ILaunch launch) {
final IDebugTarget[] debugTargets = launch.getDebugTargets();
final IProcess[] processes = launch.getProcesses();
final ILaunch currentLaunch = launch;
for (IDebugTarget element : debugTargets) {
if (element.isTerminated()) {
currentLaunch.removeDebugTarget(element);
}
}
for (IProcess element : processes) {
if (element.isTerminated()) {
currentLaunch.removeProcess(element);
}
}
}
private IDebugMessageHandler createMessageHandler(IDebugMessage message) {
if (!messageHandlers.containsKey(message.getType())) {
IDebugMessageHandler requestHandler = DebugMessagesRegistry.getHandler(message);
messageHandlers.put(message.getType(), requestHandler);
}
return messageHandlers.get(message.getType());
}
/**
* In case of a peerResponseTimeout exception we let the communication
* client handle the logic of the peerResponseTimeout.
*/
private void handlePeerResponseTimeout() {
getCommunicationClient().handlePeerResponseTimeout();
}
/**
* Start the connection with debugger.
*/
private void connect() {
requestsTable = new IntHashtable();
responseTable = new IntHashtable();
responseHandlers = new Hashtable<Integer, ResponseHandler>();
messageHandlers = new HashMap<Integer, IDebugMessageHandler>();
try {
socket.setTcpNoDelay(true);
this.connectionIn = new DataInputStream(socket.getInputStream());
this.connectionOut = new DataOutputStream(socket.getOutputStream());
messageHandler = new MessageHandler();
messageReceiver = new MessageReceiver();
// Start message handler
messageHandler.schedule();
// Start message receiver
messageReceiver.schedule();
isInitialized = true;
} catch (Exception e) {
PHPDebugPlugin.log(e);
}
}
/**
* Destroys the socket and initialize it to null.
*/
private void cleanSocket() {
if (!isInitialized)
return;
if (socket != null) {
try {
socket.shutdownInput();
} catch (Exception exc) {
// ignore
}
try {
socket.shutdownOutput();
} catch (Exception exc) {
// ignore
}
}
if (socket != null) {
try {
socket.close();
} catch (Exception exc) {
// ignore
} finally {
socket = null;
}
}
if (connectionIn != null) {
try {
synchronized (connectionIn) {
connectionIn.close();
}
} catch (Exception exc) {
// ignore
} finally {
connectionIn = null;
}
}
if (connectionOut != null) {
try {
synchronized (connectionOut) {
connectionOut.close();
}
} catch (Exception exc) {
// ignore
} finally {
connectionOut = null;
}
}
}
/**
* Terminates connection completely.
*/
private void terminate() {
if (!isConnected())
return;
// Mark it as closed already
isConnected = false;
cleanSocket();
Logger.debugMSG("DEBUG CONNECTION: Socket Cleaned"); //$NON-NLS-1$
}
private void setResponseTimeout(DebugSessionStartedNotification startedNotification) {
// Set default from preferences first
debugResponseTimeout = Platform.getPreferencesService().getInt(PHPDebugPlugin.ID,
PHPDebugCorePreferenceNames.DEBUG_RESPONSE_TIMEOUT, 60000, null);
int customResponseTimeout = ZendDebuggerSettingsUtil.getResponseTimeout(startedNotification);
if (customResponseTimeout != -1)
debugResponseTimeout = customResponseTimeout;
}
/**
* Returns true if the additional options indicated that the launch was
* triggered by a debug / profile URL command.
*/
private boolean isDebugOrProfileURL(String query) {
return (query.indexOf(DefaultDebugParametersInitializer.IS_DEBUG_URL + "=") > -1 || query //$NON-NLS-1$
.indexOf(DefaultDebugParametersInitializer.IS_PROFILE_URL + "=") > -1); //$NON-NLS-1$
}
/**
* Check if specified notification for a new session is generated for the
* same original URL as a terminated session with the same id.
*
* @param debugSessionStartedNotification
* fresh session started notification
* @param launch
* launch associated with particular session id
* @return <code>true</code> if provided notification is generated for a
* different original URL; otherwise return <code>false</code>
*/
private boolean isDifferentOriginalURL(SessionDescriptor sessionDescriptor, ILaunch launch) {
try {
// Might be null - not a web launch
String originalURL = launch.getAttribute(IDebugParametersKeys.ORIGINAL_URL);
if (originalURL == null)
return false;
URL launchUrl = new URL(originalURL);
String incomingUrl = getOriginalURL(sessionDescriptor.getStartedNotification().getOptions());
URL currentUrl = incomingUrl == null ? null : new URL(incomingUrl);
if (currentUrl != null && launchUrl != null) {
if (!currentUrl.getHost().equals(launchUrl.getHost()) || currentUrl.getPort() != launchUrl.getPort()) {
return true;
}
}
} catch (MalformedURLException e) {
PHPDebugPlugin.log(e);
}
return false;
}
/**
* Parse and return the original_url string from the debugger additional
* options.
*
* @param additionalOptions
*
* @return The original_url string
*/
protected String getOriginalURL(String additionalOptions) {
if (additionalOptions == null || additionalOptions.equals("")) { //$NON-NLS-1$
return null;
}
String optionKey = "&" + AbstractDebugParametersInitializer.ORIGINAL_URL //$NON-NLS-1$
+ "="; //$NON-NLS-1$
int startIndex = additionalOptions.indexOf(optionKey);
if (startIndex < 0) {
return null;
}
additionalOptions = additionalOptions.substring(startIndex + optionKey.length());
int endIndex = additionalOptions.indexOf('&');
if (endIndex > -1) {
additionalOptions = additionalOptions.substring(0, endIndex);
}
return additionalOptions;
}
/**
* Check for the get_file_content string in the query string. If exist, then
* the session should display the file content in the editor as a result to
* a Platform request.
*
* @param query
* The original query string arrived with the
* DebugSessionStartedNotification message.
*
* @return The file content request path, or null, if non was found.
*/
protected String getFileContentRequestPath(String query) {
return extractParameterFromQuery(query, IPHPDebugConstants.DEBUGGING_GET_FILE_CONTENT);
}
/**
* Retrieves line_number from the query string
*/
protected int getLineNumber(String query) {
try {
return Integer.parseInt(extractParameterFromQuery(query, IPHPDebugConstants.DEBUGGING_LINE_NUMBER));
} catch (NumberFormatException e) {
}
return 0;
}
/**
* Extracts parameter value from the query string
*
* @param query
* The original query string
* @param parameter
* Parameter name
* @return parameter value
*/
protected String extractParameterFromQuery(String query, String parameter) {
int queryStartIndex = query.indexOf(parameter + "="); //$NON-NLS-1$
if (queryStartIndex > -1) {
String value = query.substring(queryStartIndex + parameter.length() + 1);
int paramSeparatorIndex = value.indexOf('&');
if (paramSeparatorIndex > -1) {
value = value.substring(0, paramSeparatorIndex);
}
return value;
}
return null;
}
}