| /* |
| * Copyright (c) 2015, the Dart project authors. |
| * |
| * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html |
| * |
| * 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.dartlang.vm.service; |
| |
| import com.google.common.collect.Maps; |
| import com.google.gson.JsonElement; |
| import com.google.gson.JsonObject; |
| import com.google.gson.JsonParser; |
| import de.roderick.weberknecht.WebSocket; |
| import de.roderick.weberknecht.WebSocketEventHandler; |
| import de.roderick.weberknecht.WebSocketException; |
| import de.roderick.weberknecht.WebSocketMessage; |
| import org.dartlang.vm.service.consumer.*; |
| import org.dartlang.vm.service.element.*; |
| import org.dartlang.vm.service.internal.RequestSink; |
| import org.dartlang.vm.service.internal.VmServiceConst; |
| import org.dartlang.vm.service.internal.WebSocketRequestSink; |
| import org.dartlang.vm.service.logging.Logging; |
| |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * Internal {@link VmService} base class containing non-generated code. |
| */ |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| abstract class VmServiceBase implements VmServiceConst { |
| /** |
| * Connect to the VM observatory service via the specified URI |
| * |
| * @return an API object for interacting with the VM service (not {@code null}). |
| */ |
| public static VmService connect(final String url) throws IOException { |
| // Validate URL |
| URI uri; |
| try { |
| uri = new URI(url); |
| } catch (URISyntaxException e) { |
| throw new IOException("Invalid URL: " + url, e); |
| } |
| String wsScheme = uri.getScheme(); |
| if (!"ws".equals(wsScheme) && !"wss".equals(wsScheme)) { |
| throw new IOException("Unsupported URL scheme: " + wsScheme); |
| } |
| |
| // Create web socket and observatory |
| WebSocket webSocket; |
| try { |
| webSocket = new WebSocket(uri); |
| } catch (WebSocketException e) { |
| throw new IOException("Failed to create websocket: " + url, e); |
| } |
| final VmService vmService = new VmService(); |
| |
| // Setup event handler for forwarding responses |
| webSocket.setEventHandler(new WebSocketEventHandler() { |
| @Override |
| public void onClose() { |
| Logging.getLogger().logInformation("VM connection closed: " + url); |
| |
| vmService.connectionClosed(); |
| } |
| |
| @Override |
| public void onMessage(WebSocketMessage message) { |
| Logging.getLogger().logInformation("VM message: " + message.getText()); |
| try { |
| vmService.processMessage(message.getText()); |
| } catch (Exception e) { |
| Logging.getLogger().logError(e.getMessage(), e); |
| } |
| } |
| |
| @Override |
| public void onOpen() { |
| vmService.connectionOpened(); |
| |
| Logging.getLogger().logInformation("VM connection open: " + url); |
| } |
| |
| @Override |
| public void onPing() { |
| } |
| |
| @Override |
| public void onPong() { |
| } |
| }); |
| |
| // Establish WebSocket Connection |
| //noinspection TryWithIdenticalCatches |
| try { |
| webSocket.connect(); |
| } catch (WebSocketException e) { |
| throw new IOException("Failed to connect: " + url, e); |
| } catch (ArrayIndexOutOfBoundsException e) { |
| // The weberknecht can occasionally throw an array index exception if a connect terminates on initial connect |
| // (de.roderick.weberknecht.WebSocket.connect, WebSocket.java:126). |
| throw new IOException("Failed to connect: " + url, e); |
| } |
| vmService.requestSink = new WebSocketRequestSink(webSocket); |
| |
| // Check protocol version |
| final CountDownLatch latch = new CountDownLatch(1); |
| final String[] errMsg = new String[1]; |
| vmService.getVersion(new VersionConsumer() { |
| @Override |
| public void onError(RPCError error) { |
| String msg = "Failed to determine protocol version: " + error.getCode() + "\n message: " |
| + error.getMessage() + "\n details: " + error.getDetails(); |
| Logging.getLogger().logInformation(msg); |
| errMsg[0] = msg; |
| } |
| |
| @Override |
| public void received(Version version) { |
| vmService.runtimeVersion = version; |
| |
| latch.countDown(); |
| } |
| }); |
| |
| try { |
| if (!latch.await(5, TimeUnit.SECONDS)) { |
| throw new IOException("Failed to determine protocol version"); |
| } |
| if (errMsg[0] != null) { |
| throw new IOException(errMsg[0]); |
| } |
| } catch (InterruptedException e) { |
| throw new RuntimeException("Interrupted while waiting for response", e); |
| } |
| |
| return vmService; |
| } |
| |
| /** |
| * Connect to the VM observatory service on the given local port. |
| * |
| * @return an API object for interacting with the VM service (not {@code null}). |
| * |
| * @deprecated prefer the Url based constructor {@link VmServiceBase#connect} |
| */ |
| @Deprecated |
| public static VmService localConnect(int port) throws IOException { |
| return connect("ws://localhost:" + port + "/ws"); |
| } |
| |
| /** |
| * A mapping between {@link String} ids' and the associated {@link Consumer} that was passed when |
| * the request was made. Synchronize against {@link #consumerMapLock} before accessing this field. |
| */ |
| private final Map<String, Consumer> consumerMap = Maps.newHashMap(); |
| |
| /** |
| * The object used to synchronize access to {@link #consumerMap}. |
| */ |
| private final Object consumerMapLock = new Object(); |
| |
| /** |
| * The unique ID for the next request. |
| */ |
| private final AtomicInteger nextId = new AtomicInteger(); |
| |
| /** |
| * A list of objects to which {@link Event}s from the VM are forwarded. |
| */ |
| private final List<VmServiceListener> vmListeners = new ArrayList<>(); |
| |
| /** |
| * A list of objects to which {@link Event}s from the VM are forwarded. |
| */ |
| private final Map<String, RemoteServiceRunner> remoteServiceRunners = Maps.newHashMap(); |
| |
| /** |
| * The channel through which observatory requests are made. |
| */ |
| RequestSink requestSink; |
| |
| Version runtimeVersion; |
| |
| /** |
| * Add a listener to receive {@link Event}s from the VM. |
| */ |
| public void addVmServiceListener(VmServiceListener listener) { |
| vmListeners.add(listener); |
| } |
| |
| /** |
| * Remove the given listener from the VM. |
| */ |
| public void removeVmServiceListener(VmServiceListener listener) { |
| vmListeners.remove(listener); |
| } |
| |
| /** |
| * Add a VM RemoteServiceRunner. |
| */ |
| public void addServiceRunner(String service, RemoteServiceRunner runner) { |
| remoteServiceRunners.put(service, runner); |
| } |
| |
| /** |
| * Remove a VM RemoteServiceRunner. |
| */ |
| public void removeServiceRunner(String service) { |
| remoteServiceRunners.remove(service); |
| } |
| |
| /** |
| * Return the VM service protocol version supported by the current debug connection. |
| */ |
| public Version getRuntimeVersion() { |
| return runtimeVersion; |
| } |
| |
| /** |
| * Disconnect from the VM observatory service. |
| */ |
| public void disconnect() { |
| requestSink.close(); |
| } |
| |
| /** |
| * Return the instance with the given identifier. |
| */ |
| public void getInstance(String isolateId, String instanceId, final GetInstanceConsumer consumer) { |
| getObject(isolateId, instanceId, new GetObjectConsumer() { |
| |
| @Override |
| public void onError(RPCError error) { |
| consumer.onError(error); |
| } |
| |
| @Override |
| public void received(Obj response) { |
| if (response instanceof Instance) { |
| consumer.received((Instance) response); |
| } else { |
| onError(RPCError.unexpected("Instance", response)); |
| } |
| } |
| |
| @Override |
| public void received(Sentinel response) { |
| onError(RPCError.unexpected("Instance", response)); |
| } |
| }); |
| } |
| |
| /** |
| * Return the library with the given identifier. |
| */ |
| public void getLibrary(String isolateId, String libraryId, final GetLibraryConsumer consumer) { |
| getObject(isolateId, libraryId, new GetObjectConsumer() { |
| |
| @Override |
| public void onError(RPCError error) { |
| consumer.onError(error); |
| } |
| |
| @Override |
| public void received(Obj response) { |
| if (response instanceof Library) { |
| consumer.received((Library) response); |
| } else { |
| onError(RPCError.unexpected("Library", response)); |
| } |
| } |
| |
| @Override |
| public void received(Sentinel response) { |
| onError(RPCError.unexpected("Library", response)); |
| } |
| }); |
| } |
| |
| public abstract void getObject(String isolateId, String objectId, GetObjectConsumer consumer); |
| |
| /** |
| * Invoke a specific service protocol extension method. |
| * <p> |
| * See https://api.dart.dev/stable/dart-developer/dart-developer-library.html. |
| */ |
| public void callServiceExtension(String isolateId, String method, ServiceExtensionConsumer consumer) { |
| JsonObject params = new JsonObject(); |
| params.addProperty("isolateId", isolateId); |
| request(method, params, consumer); |
| } |
| |
| /** |
| * Invoke a specific service protocol extension method. |
| * <p> |
| * See https://api.dart.dev/stable/dart-developer/dart-developer-library.html. |
| */ |
| public void callServiceExtension(String isolateId, String method, JsonObject params, ServiceExtensionConsumer consumer) { |
| params.addProperty("isolateId", isolateId); |
| request(method, params, consumer); |
| } |
| |
| /** |
| * Sends the request and associates the request with the passed {@link Consumer}. |
| */ |
| protected void request(String method, JsonObject params, Consumer consumer) { |
| |
| // Assemble the request |
| String id = Integer.toString(nextId.incrementAndGet()); |
| JsonObject request = new JsonObject(); |
| |
| request.addProperty(JSONRPC, JSONRPC_VERSION); |
| request.addProperty(ID, id); |
| request.addProperty(METHOD, method); |
| request.add(PARAMS, params); |
| |
| // Cache the consumer to receive the response |
| synchronized (consumerMapLock) { |
| consumerMap.put(id, consumer); |
| } |
| |
| // Send the request |
| requestSink.add(request); |
| } |
| |
| public void connectionOpened() { |
| for (VmServiceListener listener : new ArrayList<>(vmListeners)) { |
| try { |
| listener.connectionOpened(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Exception notifying listener", e); |
| } |
| } |
| } |
| |
| private void forwardEvent(String streamId, Event event) { |
| for (VmServiceListener listener : new ArrayList<>(vmListeners)) { |
| try { |
| listener.received(streamId, event); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Exception processing event: " + streamId + ", " + event.getJson(), e); |
| } |
| } |
| } |
| |
| public void connectionClosed() { |
| for (VmServiceListener listener : new ArrayList<>(vmListeners)) { |
| try { |
| listener.connectionClosed(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Exception notifying listener", e); |
| } |
| } |
| } |
| |
| abstract void forwardResponse(Consumer consumer, String type, JsonObject json); |
| |
| void logUnknownResponse(Consumer consumer, JsonObject json) { |
| Class<? extends Consumer> consumerClass = consumer.getClass(); |
| StringBuilder msg = new StringBuilder(); |
| msg.append("Expected response for ").append(consumerClass).append("\n"); |
| for (Class<?> interf : consumerClass.getInterfaces()) { |
| msg.append(" implementing ").append(interf).append("\n"); |
| } |
| msg.append(" but received ").append(json); |
| Logging.getLogger().logError(msg.toString()); |
| } |
| |
| /** |
| * Process the response from the VM service and forward that response to the consumer associated |
| * with the response id. |
| */ |
| void processMessage(String jsonText) { |
| if (jsonText == null || jsonText.isEmpty()) { |
| return; |
| } |
| |
| // Decode the JSON |
| JsonObject json; |
| try { |
| json = (JsonObject) new JsonParser().parse(jsonText); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Parse message failed: " + jsonText, e); |
| return; |
| } |
| |
| if (json.has("method")) { |
| if (!json.has(PARAMS)) { |
| final String message = "Missing " + PARAMS; |
| Logging.getLogger().logError(message); |
| final JsonObject response = new JsonObject(); |
| response.addProperty(JSONRPC, JSONRPC_VERSION); |
| final JsonObject error = new JsonObject(); |
| error.addProperty(CODE, INVALID_REQUEST); |
| error.addProperty(MESSAGE, message); |
| response.add(ERROR, error); |
| requestSink.add(response); |
| return; |
| } |
| if (json.has("id")) { |
| processRequest(json); |
| } else { |
| processNotification(json); |
| } |
| } else if (json.has("result") || json.has("error")) { |
| processResponse(json); |
| } else { |
| Logging.getLogger().logError("Malformed message"); |
| } |
| } |
| |
| void processRequest(JsonObject json) { |
| final JsonObject response = new JsonObject(); |
| response.addProperty(JSONRPC, JSONRPC_VERSION); |
| |
| // Get the consumer associated with this request |
| String id; |
| try { |
| id = json.get(ID).getAsString(); |
| } catch (Exception e) { |
| final String message = "Request malformed " + ID; |
| Logging.getLogger().logError(message, e); |
| final JsonObject error = new JsonObject(); |
| error.addProperty(CODE, INVALID_REQUEST); |
| error.addProperty(MESSAGE, message); |
| response.add(ERROR, error); |
| requestSink.add(response); |
| return; |
| } |
| |
| response.addProperty(ID, id); |
| |
| String method; |
| try { |
| method = json.get(METHOD).getAsString(); |
| } catch (Exception e) { |
| final String message = "Request malformed " + METHOD; |
| Logging.getLogger().logError(message, e); |
| final JsonObject error = new JsonObject(); |
| error.addProperty(CODE, INVALID_REQUEST); |
| error.addProperty(MESSAGE, message); |
| response.add(ERROR, error); |
| requestSink.add(response); |
| return; |
| } |
| |
| JsonObject params; |
| try { |
| params = json.get(PARAMS).getAsJsonObject(); |
| } catch (Exception e) { |
| final String message = "Request malformed " + METHOD; |
| Logging.getLogger().logError(message, e); |
| final JsonObject error = new JsonObject(); |
| error.addProperty(CODE, INVALID_REQUEST); |
| error.addProperty(MESSAGE, message); |
| response.add(ERROR, error); |
| requestSink.add(response); |
| return; |
| } |
| |
| if (!remoteServiceRunners.containsKey(method)) { |
| final String message = "Unknown service " + method; |
| Logging.getLogger().logError(message); |
| final JsonObject error = new JsonObject(); |
| error.addProperty(CODE, METHOD_NOT_FOUND); |
| error.addProperty(MESSAGE, message); |
| response.add(ERROR, error); |
| requestSink.add(response); |
| return; |
| } |
| |
| final RemoteServiceRunner runner = remoteServiceRunners.get(method); |
| try { |
| runner.run(params, new RemoteServiceCompleter() { |
| public void result(JsonObject result) { |
| response.add(RESULT, result); |
| requestSink.add(response); |
| } |
| |
| public void error(int code, String message, JsonObject data) { |
| final JsonObject error = new JsonObject(); |
| error.addProperty(CODE, code); |
| error.addProperty(MESSAGE, message); |
| if (data != null) { |
| error.add(DATA, data); |
| } |
| response.add(ERROR, error); |
| requestSink.add(response); |
| } |
| }); |
| } catch (Exception e) { |
| final String message = "Internal Server Error"; |
| Logging.getLogger().logError(message, e); |
| final JsonObject error = new JsonObject(); |
| error.addProperty(CODE, SERVER_ERROR); |
| error.addProperty(MESSAGE, message); |
| response.add(ERROR, error); |
| requestSink.add(response); |
| } |
| } |
| |
| private static final RemoteServiceCompleter ignoreCallback = |
| new RemoteServiceCompleter() { |
| public void result(JsonObject result) { |
| // ignore |
| } |
| |
| public void error(int code, String message, JsonObject data) { |
| // ignore |
| } |
| }; |
| |
| void processNotification(JsonObject json) { |
| String method; |
| try { |
| method = json.get(METHOD).getAsString(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Request malformed " + METHOD, e); |
| return; |
| } |
| JsonObject params; |
| try { |
| params = json.get(PARAMS).getAsJsonObject(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Event missing " + PARAMS, e); |
| return; |
| } |
| if ("streamNotify".equals(method)) { |
| String streamId; |
| try { |
| streamId = params.get(STREAM_ID).getAsString(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Event missing " + STREAM_ID, e); |
| return; |
| } |
| Event event; |
| try { |
| event = new Event(params.get(EVENT).getAsJsonObject()); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Event missing " + EVENT, e); |
| return; |
| } |
| forwardEvent(streamId, event); |
| } else { |
| if (!remoteServiceRunners.containsKey(method)) { |
| Logging.getLogger().logError("Unknown service " + method); |
| return; |
| } |
| |
| final RemoteServiceRunner runner = remoteServiceRunners.get(method); |
| try { |
| runner.run(params, ignoreCallback); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Internal Server Error", e); |
| } |
| } |
| } |
| |
| void processResponse(JsonObject json) { |
| JsonElement idElem = json.get(ID); |
| if (idElem == null) { |
| Logging.getLogger().logError("Response missing " + ID); |
| return; |
| } |
| |
| // Get the consumer associated with this response |
| String id; |
| try { |
| id = idElem.getAsString(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Response missing " + ID, e); |
| return; |
| } |
| Consumer consumer = consumerMap.remove(id); |
| if (consumer == null) { |
| Logging.getLogger().logError("No consumer associated with " + ID + ": " + id); |
| return; |
| } |
| |
| // Forward the response if the request was successfully executed |
| JsonElement resultElem = json.get(RESULT); |
| if (resultElem != null) { |
| JsonObject result; |
| try { |
| result = resultElem.getAsJsonObject(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Response has invalid " + RESULT, e); |
| return; |
| } |
| String responseType = ""; |
| if (result.has(TYPE)) { |
| responseType = result.get(TYPE).getAsString(); |
| } |
| // ServiceExtensionConsumers do not care about the response type. |
| else if (!(consumer instanceof ServiceExtensionConsumer)) { |
| Logging.getLogger().logError("Response missing " + TYPE + ": " + result.toString()); |
| return; |
| } |
| forwardResponse(consumer, responseType, result); |
| return; |
| } |
| |
| // Forward an error if the request failed |
| resultElem = json.get(ERROR); |
| if (resultElem != null) { |
| JsonObject error; |
| try { |
| error = resultElem.getAsJsonObject(); |
| } catch (Exception e) { |
| Logging.getLogger().logError("Response has invalid " + RESULT, e); |
| return; |
| } |
| consumer.onError(new RPCError(error)); |
| return; |
| } |
| |
| Logging.getLogger().logError("Response missing " + RESULT + " and " + ERROR); |
| } |
| } |