| /* |
| * 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<String, String> 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); |
| } |
| } |
| } |
| } |