blob: 9301000dcf6d3507761d5a4befc5d4b6992c6b15 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.lib.v8debug.connection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.simple.JSONObject;
import org.json.simple.parser.ContainerFactory;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.netbeans.lib.v8debug.JSONReader;
import org.netbeans.lib.v8debug.JSONWriter;
import org.netbeans.lib.v8debug.V8Command;
import org.netbeans.lib.v8debug.V8Event;
import org.netbeans.lib.v8debug.V8Request;
import org.netbeans.lib.v8debug.V8Response;
import static org.netbeans.lib.v8debug.connection.DebuggerConnection.*;
/**
* A debugger server connection. This is a main server debugger class.
* Create an instance of this class to listen for incoming debugger connections.
* <p>
* The typical usage is:
* <pre><tt>
* final ServerConnection sn = new ServerConnection();
* final Map&lt;String, String&gt; properties = ... // See HeaderProperties.
* new Thread() {
* public void run() {
* try {
* sn.runConnectionLoop(map, new ServerConnection.Listener() {
* public ServerConnection.ResponseProvider request(V8Request request) {
* return ServerConnection.ResponseProvider.create(
* request.createSuccessResponse(...));
* }
* });
* } catch (IOException ex) {
* ...
* }
* }
* }.start();
* ...
* V8Event event = new V8Event(...);
* sn.send(event);
* </tt></pre>
*
* @author Martin Entlicher
*/
public final class ServerConnection {
private static final Logger LOG = Logger.getLogger(ServerConnection.class.getName());
private static final String SERVER_PROTOCOL_VERSION = "1";
private final ServerSocket server;
private Socket currentSocket;
private InputStream clientIn;
private OutputStream clientOut;
private final Object outLock = new Object();
private final byte[] buffer = new byte[BUFFER_SIZE];
private final ContainerFactory containerFactory = new LinkedJSONContainterFactory();
private final Set<IOListener> ioListeners = new CopyOnWriteArraySet<>();
/**
* Create a new server listening connection with automatically selected port
* number. The actual port number that was selected can be retrieved by
* calling {@link #getPort()}.
* @throws IOException when an IO problem occurs.
*/
public ServerConnection() throws IOException {
server = new ServerSocket(0);
}
/**
* Create a new server listening connection on the specific port.
* @param serverPort The port the connection is listening on.
* @throws IOException when an IO problem occurs.
* @throws IllegalArgumentException if the port parameter is outside the
* specified range of valid port values, which is between 0 and 65535, inclusive.
*/
public ServerConnection(int serverPort) throws IOException, IllegalArgumentException {
server = new ServerSocket(serverPort);
}
/**
* Execute the debugger events loop. Run this in an application thread, this
* class does not provide any threading. This method waits until some client
* debugger is connected, keeps processing the debugger requests and keeps
* blocking until the connection is closed. Then the method can be called
* again to accept another client debugger connection.
* @param properties The map of properties to provide to the client as a header.
* See {@link HeaderProperties}
* @param listener The listener to receive the debugger events.
* @throws IOException thrown when an IO problem occurs.
*/
public void runConnectionLoop(Map<String, String> properties, Listener listener) throws IOException {
Socket socket = server.accept();
socket.setTcpNoDelay(true);
currentSocket = socket;
clientIn = socket.getInputStream();
clientOut = socket.getOutputStream();
sendProperties(properties);
runEventLoop(listener);
}
/**
* Get the port number this connection is listening on.
* @return The port number.
*/
public int getPort() {
return server.getLocalPort();
}
private void runEventLoop(Listener listener) throws IOException {
int n;
int contentLength = -1;
int[] beginPos = new int[] { 0 };
int[] fromPtr = new int[] { 0 };
int readOffset = 0;
String tools = null;
byte[] emptyArray = new byte[] {};
byte[] messageBytes = emptyArray;
while ((n = clientIn.read(buffer, readOffset, BUFFER_SIZE - readOffset)) > 0) {
n += readOffset;
int from = 0;
do {
if (contentLength < 0) {
fromPtr[0] = from;
contentLength = readContentLength(buffer, fromPtr, n, beginPos);
if (contentLength < 0) {
break;
}
from = fromPtr[0];
}
if (tools == null) {
fromPtr[0] = from;
tools = readTools(buffer, fromPtr, n);
if (tools == null) {
break;
} else {
from = fromPtr[0];
}
}
int length = Math.min(contentLength - messageBytes.length, n - from);
messageBytes = Utils.joinArrays(messageBytes, buffer, from, length);
from += length;
if (messageBytes.length == contentLength) {
String message = new String(messageBytes, CHAR_SET);
try {
received(listener, tools, message);
} catch (ThreadDeath td) {
throw td;
} catch (ParseException pex) {
throw new IOException(pex.getLocalizedMessage(), pex);
} catch (Throwable t) {
LOG.log(Level.SEVERE, message, t);
}
contentLength = -1;
tools = null;
messageBytes = emptyArray;
}
} while (from < n);
if (from < n) {
System.arraycopy(buffer, from, buffer, 0, n - from);
readOffset = n - from;
} else {
readOffset = 0;
}
}
}
private void received(Listener listener, String tools, String message) throws ParseException, IOException {
//System.out.println("RECEIVED: tools: '"+tools+"', message: '"+message+"'");
fireReceived(message);
LOG.log(Level.FINE, "RECEIVED: {0}, {1}", new Object[]{tools, message});
if (message.isEmpty()) {
return ;
}
JSONParser parser = new JSONParser();
JSONObject obj = (JSONObject) parser.parse(message, containerFactory);
V8Request request = JSONReader.getRequest(obj);
ResponseProvider rp = listener.request(request);
if (V8Command.Disconnect.equals(request.getCommand())) {
try {
closeCurrentConnection();
} catch (IOException ioex) {}
}
if (rp != null) {
rp.sendTo(this);
}
}
private void sendProperties(Map<String, String> properties) throws IOException {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> prop : properties.entrySet()) {
sb.append(prop.getKey());
sb.append(": ");
sb.append(prop.getValue());
sb.append(EOL_STR);
}
if (!properties.keySet().contains(HeaderProperties.PROTOCOL_VERSION)) {
sb.append(HeaderProperties.PROTOCOL_VERSION +
": "+SERVER_PROTOCOL_VERSION +
EOL_STR);
}
sb.append(CONTENT_LENGTH_STR+"0" + EOL_STR + EOL_STR);
byte[] bytes = sb.toString().getBytes(CHAR_SET);
synchronized (outLock) {
clientOut.write(bytes);
}
}
private void send(V8Response response) throws IOException {
JSONObject obj = JSONWriter.store(response);
sendJSON(obj);
}
/**
* Send a debugger event.
* @param event An event to send to the client.
* @throws IOException thrown when an IO problem occurs.
*/
public void send(V8Event event) throws IOException {
JSONObject obj = JSONWriter.store(event);
sendJSON(obj);
}
private void sendJSON(JSONObject obj) throws IOException {
String text = obj.toJSONString();
text = text.replace("\\/", "/"); // Replace escaped slash "\/" with shash "/". Unescape slashes.
//System.out.println("SEND: "+text);
fireSent(text);
LOG.log(Level.FINE, "SEND: {0}", text);
byte[] bytes = text.getBytes(CHAR_SET);
String contentLength = CONTENT_LENGTH_STR+bytes.length + EOL_STR + EOL_STR;
synchronized (outLock) {
if (clientOut == null) {
throw new IOException("No client connection is opened.");
}
clientOut.write(contentLength.getBytes(CHAR_SET));
clientOut.write(bytes);
}
}
public boolean isConnected() {
return currentSocket != null && clientOut != null;
}
/**
* Close the currently established client-server connection, if any.
* The {@link #runConnectionLoop(java.util.Map, org.netbeans.lib.v8debug.connection.ServerConnection.Listener)}
* can then be executed again to accept another client connection.
* @throws IOException thrown when an IO problem occurs.
*/
public void closeCurrentConnection() throws IOException {
if (currentSocket != null) {
currentSocket.close();
currentSocket = null;
}
}
/**
* Close the server connection. Stop accepting incoming connections.
* @throws IOException thrown when an IO problem occurs.
*/
public void closeServer() throws IOException {
if (server != null) {
server.close();
}
fireClosed();
}
/**
* Add an I/O listener to monitor the debugger communication.
* @param iol an IOListener
*/
public void addIOListener(IOListener iol) {
ioListeners.add(iol);
}
/**
* Remove an I/O listener monitoring the communication.
* @param iol an IOListener
*/
public void removeIOListener(IOListener iol) {
ioListeners.remove(iol);
}
private void fireSent(String str) {
for (IOListener iol : ioListeners) {
iol.sent(str);
}
}
private void fireReceived(String str) {
for (IOListener iol : ioListeners) {
iol.received(str);
}
}
private void fireClosed() {
for (IOListener iol : ioListeners) {
iol.closed();
}
}
/**
* Listener receiving debugger events.
*/
public interface Listener {
/**
* Called when a request is received. The implementation should compose
* a response and provide it either synchronously or asynchronously via
* the returned {@link ResponseProvider}.
* @param request The received request.
* @return The response provider allowing either synchronous or asynchronous response.
*/
ResponseProvider request(V8Request request);
}
/**
* Debugger response provider. Allows synchronous or asynchronous responses.
* Create the response by calling
* {@link V8Request#createSuccessResponse(long, org.netbeans.lib.v8debug.V8Body, org.netbeans.lib.v8debug.vars.ReferencedValue[], boolean)}
* or {@link V8Request#createErrorResponse(long, boolean, java.lang.String)}.
*/
public static final class ResponseProvider {
private V8Response response;
private ServerConnection sc;
private ResponseProvider(V8Response response) {
this.response = response;
}
/**
* Create a synchronous response to a debugger request.
* @param response The response.
* Use {@link V8Request#createSuccessResponse(long, org.netbeans.lib.v8debug.V8Body, org.netbeans.lib.v8debug.vars.ReferencedValue[], boolean)}
* or {@link V8Request#createErrorResponse(long, boolean, java.lang.String)}
* to create the response.
* @return A synchronous response provider.
*/
public static ResponseProvider create(V8Response response) {
return new ResponseProvider(response);
}
/**
* Create an ampty asynchronous response provider.
* @return an asynchronous response, call
* {@link #setResponse(org.netbeans.lib.v8debug.V8Response)} on the
* returned object to set the response asynchronously.
*/
public static ResponseProvider createLazy() {
return new ResponseProvider(null);
}
/**
* Set the asynchronous response.
* @param response The response.
* Use {@link V8Request#createSuccessResponse(long, org.netbeans.lib.v8debug.V8Body, org.netbeans.lib.v8debug.vars.ReferencedValue[], boolean)}
* or {@link V8Request#createErrorResponse(long, boolean, java.lang.String)}
* to create the response.
* @throws IOException thrown when an IO problem occurs.
*/
public void setResponse(V8Response response) throws IOException {
ServerConnection sc;
synchronized (this) {
if (this.response != null) {
throw new IllegalStateException("Response has been set already.");
}
this.response = response;
sc = this.sc;
}
if (sc != null) {
sc.send(response);
}
}
void sendTo(ServerConnection sc) throws IOException {
V8Response response;
synchronized (this) {
response = this.response;
this.sc = sc;
}
if (response != null) {
sc.send(response);
}
}
}
}