/******************************************************************************* * Copyright (c) 2016 Rogue Wave Software, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Rogue Wave Software, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.zdb.server.connection; import static org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.NOTIFICATION_CLOSE_SESSION; import static org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.NOTIFICATION_CLOSE_MESSAGE_HANDLER; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import org.eclipse.che.plugin.zdb.server.ZendDebugger; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.IDbgClientMessage; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.IDbgClientNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.IDbgClientRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgClientMessages.IDbgClientResponse; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.CloseMessageHandlerNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.IDbgEngineMessage; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.IDbgEngineNotification; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.IDbgEngineRequest; import org.eclipse.che.plugin.zdb.server.connection.ZendDbgEngineMessages.IDbgEngineResponse; import org.eclipse.che.plugin.zdb.server.exceptions.ZendDbgTimeoutException; /** * The debug connection is responsible for initializing and handling a debug * session that was triggered by debugger engine. * * @author Bartlomiej Laczkowski */ public class ZendDbgConnection { private final class EngineConnectionRunnable implements Runnable { private ServerSocket debugSocket; private Socket socket; private DataInputStream inputStream; private DataOutputStream outputStream; private EngineConnectionRunnable() { open(); } @Override public void run() { while (!debugSocket.isClosed()) { try (Socket socket = debugSocket.accept(); DataInputStream inputStream = new DataInputStream(socket.getInputStream()); DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());) { socket.setReceiveBufferSize(1024 * 128); socket.setSendBufferSize(1024 * 128); socket.setTcpNoDelay(true); this.socket = socket; this.inputStream = inputStream; this.outputStream = outputStream; read(); } catch (Exception e) { if (debugSocket.isClosed()) { break; } ZendDebugger.LOG.error(e.getMessage(), e); } } engineMessageRunnable.queue(new CloseMessageHandlerNotification()); } private void open() { try { if (debugSettings.isUseSsslEncryption()) { SSLServerSocket sslServerSocket = (SSLServerSocket) SSLServerSocketFactory.getDefault() .createServerSocket(debugSettings.getDebugPort()); sslServerSocket.setEnabledCipherSuites(sslServerSocket.getSupportedCipherSuites()); this.debugSocket = sslServerSocket; } else { this.debugSocket = new ServerSocket(debugSettings.getDebugPort()); } } catch (Exception e) { ZendDebugger.LOG.error(e.getMessage(), e); } } private void read() { isConnected = true; while (true) { try { // Reads the message length int length = inputStream.readInt(); if (length < 0) { ZendDebugger.LOG.error( "Zend debugger engine is probably using SSL, please check 'Use SSL encryption' option."); purge(); break; } // Engine message arrived. read its type identifier. int messageType = inputStream.readShort(); IDbgEngineMessage engineMessage = ZendDbgEngineMessages.create(messageType); engineMessage.deserialize(inputStream); if (engineMessage instanceof IDbgEngineResponse) { // Engine response has arrived... IDbgEngineResponse response = (IDbgEngineResponse) engineMessage; EngineSyncResponse<IDbgEngineResponse> syncResponse = engineSyncResponses .remove(response.getID()); if (syncResponse != null) { // Release waiting request provider syncResponse.set(response); continue; } } else { // Notification or request from engine arrived... engineMessageRunnable.queue(engineMessage); } } catch (EOFException e) { // Engine closed the session break; } catch (Exception e) { ZendDebugger.LOG.error(e.getMessage(), e); break; } } isConnected = false; } private void write(IDbgClientMessage clientMessage) { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream); try { clientMessage.serialize(dataOutputStream); int messageSize = byteArrayOutputStream.size(); // Write to connection output outputStream.writeInt(messageSize); byteArrayOutputStream.writeTo(outputStream); outputStream.flush(); } catch (Exception e) { ZendDebugger.LOG.error(e.getMessage(), e); } if (clientMessage.getType() == NOTIFICATION_CLOSE_SESSION) { purge(); } } private void purge() { if (socket != null && !socket.isClosed()) { try { socket.shutdownInput(); socket.shutdownOutput(); } catch (Exception e) { // ignore } } } private void close() { try { purge(); debugSocket.close(); } catch (IOException e) { ZendDebugger.LOG.error(e.getMessage(), e); } } } private final class EngineMessageRunnable implements Runnable { private BlockingQueue<IDbgEngineMessage> messageQueue = new ArrayBlockingQueue<IDbgEngineMessage>(100); @Override public void run() { while (true) { try { IDbgEngineMessage message = messageQueue.take(); if (message.getType() == NOTIFICATION_CLOSE_MESSAGE_HANDLER) break; if (message instanceof IDbgEngineNotification) { engineMessageHandler.handleNotification((IDbgEngineNotification) message); } else if (message instanceof IDbgEngineRequest) { IDbgClientResponse response = engineMessageHandler .handleRequest((IDbgEngineRequest<?>) message); engineConnectionRunnable.write(response); } } catch (Exception e) { ZendDebugger.LOG.error(e.getMessage(), e); } } } private void queue(IDbgEngineMessage m) { messageQueue.offer(m); } } private static final class EngineSyncResponse<T extends IDbgEngineResponse> { private static final int TIMEOUT = 10; private final Semaphore semaphore = new Semaphore(0);; private T response; void set(T response) { this.response = response; semaphore.release(); } T get() throws ZendDbgTimeoutException { boolean acquired = false; try { acquired = semaphore.tryAcquire(10, TimeUnit.SECONDS); } catch (InterruptedException e) { ZendDebugger.LOG.error(e.getMessage(), e); } if (acquired) { return this.response; } throw new ZendDbgTimeoutException(TIMEOUT, TimeUnit.SECONDS); } } public interface IEngineMessageHandler { void handleNotification(IDbgEngineNotification notification); <T extends IDbgClientResponse> T handleRequest(IDbgEngineRequest<T> request); } private EngineConnectionRunnable engineConnectionRunnable; private ExecutorService engineConnectionRunnableExecutor; private EngineMessageRunnable engineMessageRunnable; private ExecutorService engineMessageRunnableExecutor; private Map<Integer, EngineSyncResponse<IDbgEngineResponse>> engineSyncResponses = new HashMap<>(); private final ZendDbgSettings debugSettings; private IEngineMessageHandler engineMessageHandler; private int debugRequestId = 1000; private boolean isConnected = false; /** * Constructs a new DebugConnectionThread with a given Socket. * * @param socket */ public ZendDbgConnection(IEngineMessageHandler engineMessageHandler, ZendDbgSettings debugSettings) { this.engineMessageHandler = engineMessageHandler; this.debugSettings = debugSettings; } /** * Start the connection with debugger. Causes connection & message handler * threads to be started. */ public void connect() { try { engineConnectionRunnableExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { final Thread thread = new Thread(r, "ZendDbgClient:" + debugSettings.getDebugPort()); thread.setDaemon(true); return thread; } }); engineMessageRunnableExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { final Thread thread = new Thread(r, "ZendDbgMsgHandler"); thread.setDaemon(true); return thread; } }); engineConnectionRunnable = new EngineConnectionRunnable(); engineMessageRunnable = new EngineMessageRunnable(); engineConnectionRunnableExecutor.execute(engineConnectionRunnable); engineMessageRunnableExecutor.execute(engineMessageRunnable); } catch (Exception e) { ZendDebugger.LOG.error(e.getMessage(), e); } } /** * Closes the connection. Causes connection & message handler threads to be * shutdown. */ public void disconnect() { engineConnectionRunnable.close(); engineConnectionRunnableExecutor.shutdown(); engineMessageRunnableExecutor.shutdown(); } /** * Sends given client request to Zend debugger engine. * * @param request * @return Zend debugger engine response */ @SuppressWarnings("unchecked") public synchronized <T extends IDbgEngineResponse> T sendRequest(IDbgClientRequest<T> request) { if (!isConnected) { return null; } try { request.setID(debugRequestId++); EngineSyncResponse<T> syncResponse = new EngineSyncResponse<T>(); engineSyncResponses.put(request.getID(), (EngineSyncResponse<IDbgEngineResponse>) syncResponse); engineConnectionRunnable.write(request); // Wait for response return syncResponse.get(); } catch (ZendDbgTimeoutException e) { ZendDebugger.LOG.error("Could not get debugger engine response for " + request.toString(), e); } catch (Exception e) { ZendDebugger.LOG.error(e.getMessage(), e); } return null; } /** * Sends given client notification to Zend debugger engine. * * @param request */ public synchronized void sendNotification(IDbgClientNotification request) { if (!isConnected) { return; } try { engineConnectionRunnable.write(request); } catch (Exception e) { ZendDebugger.LOG.error(e.getMessage(), e); } } }