/* * Copyright 2017 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ package io.flutter.inspector; import com.google.gson.JsonObject; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.util.Alarm; import com.intellij.xdebugger.XSourcePosition; import io.flutter.utils.StreamSubscription; import io.flutter.vmService.DartVmServiceDebugProcess; import io.flutter.vmService.VMServiceManager; import org.dartlang.vm.service.VmService; import org.dartlang.vm.service.consumer.EvaluateConsumer; import org.dartlang.vm.service.consumer.GetIsolateConsumer; import org.dartlang.vm.service.consumer.GetObjectConsumer; import org.dartlang.vm.service.consumer.ServiceExtensionConsumer; import org.dartlang.vm.service.element.*; import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; /** * Invoke methods from a specified Dart library using the observatory protocol. */ public class EvalOnDartLibrary implements Disposable { private static final Logger LOG = Logger.getInstance(EvalOnDartLibrary.class); private final StreamSubscription<IsolateRef> subscription; private final ScheduledThreadPoolExecutor delayer; private String isolateId; private final VmService vmService; @SuppressWarnings("FieldCanBeLocal") private final VMServiceManager vmServiceManager; private final Set<String> libraryNames; CompletableFuture<LibraryRef> libraryRef; private final Alarm myRequestsScheduler; static final int DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; /** * For robustness we ensure at most one pending request is issued at a time. */ private CompletableFuture<?> allPendingRequestsDone; private final Object pendingRequestLock = new Object(); /** * Public so that other related classes such as InspectorService can ensure their * requests are in a consistent order with requests which eliminates otherwise * surprising timing bugs such as if a request to dispose an * InspectorService.ObjectGroup was issued after a request to read properties * from an object in a group but the request to dispose the object group * occurred first. * <p> * The design is we have at most 1 pending request at a time. This sacrifices * some throughput with the advantage of predictable semantics and the benefit * that we are able to skip large numbers of requests if they happen to be * from groups of objects that should no longer be kept alive. * <p> * The optional ObjectGroup specified by isAlive, indicates whether the * request is still relevant or should be cancelled. This is an optimization * for the Inspector to avoid overloading the service with stale requests if * the user is quickly navigating through the UI generating lots of stale * requests to view specific details subtrees. */ public <T> CompletableFuture<T> addRequest(InspectorService.ObjectGroup isAlive, String requestName, Supplier<CompletableFuture<T>> request) { if (isAlive != null && isAlive.isDisposed()) { return CompletableFuture.completedFuture(null); } if (myRequestsScheduler.isDisposed()) { return CompletableFuture.completedFuture(null); } // Future that completes when the request has finished. final CompletableFuture<T> response = new CompletableFuture<>(); // This is an optimization to avoid sending stale requests across the wire. final Runnable wrappedRequest = () -> { if (isAlive != null && isAlive.isDisposed()) { response.complete(null); return; } // No need to timeout until the request has actually started. timeoutAfter(response, DEFAULT_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS, requestName); final CompletableFuture<T> future = request.get(); future.whenCompleteAsync((v, t) -> { if (t != null) { response.completeExceptionally(t); } else { response.complete(v); } }); }; synchronized (pendingRequestLock) { if (allPendingRequestsDone == null || allPendingRequestsDone.isDone()) { allPendingRequestsDone = response; myRequestsScheduler.addRequest(wrappedRequest, 0); } else { final CompletableFuture<?> previousDone = allPendingRequestsDone; allPendingRequestsDone = response; // Actually schedule this request only after the previous request completes. previousDone.whenCompleteAsync((v, error) -> { if (myRequestsScheduler.isDisposed()) { response.complete(null); } else { myRequestsScheduler.addRequest(wrappedRequest, 0); } }); } } return response; } public EvalOnDartLibrary(Set<String> libraryNames, VmService vmService, VMServiceManager vmServiceManager) { this.libraryNames = libraryNames; this.vmService = vmService; this.vmServiceManager = vmServiceManager; this.myRequestsScheduler = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this); libraryRef = new CompletableFuture<>(); subscription = vmServiceManager.getCurrentFlutterIsolate((isolate) -> { if (libraryRef.isDone()) { libraryRef = new CompletableFuture<>(); } if (isolate != null) { initialize(isolate.getId()); } }, true); delayer = new ScheduledThreadPoolExecutor(1); } public String getIsolateId() { return isolateId; } CompletableFuture<LibraryRef> getLibraryRef() { return libraryRef; } public void dispose() { subscription.dispose(); // TODO(jacobr): complete all pending futures as cancelled? } public CompletableFuture<JsonObject> invokeServiceMethod(String method, JsonObject params) { final CompletableFuture<JsonObject> ret = new CompletableFuture<>(); timeoutAfter(ret, DEFAULT_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS, "service method " + method); vmService.callServiceExtension(isolateId, method, params, new ServiceExtensionConsumer() { @Override public void onError(RPCError error) { ret.completeExceptionally(new RuntimeException(error.getMessage())); } @Override public void received(JsonObject object) { ret.complete(object); } }); return ret; } // TODO(jacobr): remove this method after we switch to Java9+ which supports this method directly on CompletableFuture. private void timeoutAfter(CompletableFuture<?> future, long timeout, TimeUnit unit, String operationName) { // Create the timeout exception now, so we can capture the stack trace of the caller. final TimeoutException timeoutException = new TimeoutException(operationName); delayer.schedule(() -> future.completeExceptionally(timeoutException), timeout, unit); } public CompletableFuture<InstanceRef> eval(String expression, Map<String, String> scope, InspectorService.ObjectGroup isAlive) { return addRequest(isAlive, "evaluate", () -> { final CompletableFuture<InstanceRef> future = new CompletableFuture<>(); libraryRef.thenAcceptAsync((LibraryRef ref) -> vmService.evaluate( getIsolateId(), ref.getId(), expression, scope, true, new EvaluateConsumer() { @Override public void onError(RPCError error) { future.completeExceptionally( new EvalException(expression, Integer.toString(error.getCode()), error.getMessage())); } @Override public void received(ErrorRef response) { future.completeExceptionally( new EvalException(expression, response.getKind().name(), response.getMessage())); } @Override public void received(InstanceRef response) { future.complete(response); } @Override public void received(Sentinel response) { future.completeExceptionally( new EvalException(expression, "Sentinel", response.getValueAsString())); } } )); return future; }); } @SuppressWarnings("unchecked") public <T extends Obj> CompletableFuture<T> getObjectHelper(ObjRef instance, InspectorService.ObjectGroup isAlive) { return addRequest(isAlive, "getObject", () -> { final CompletableFuture<T> future = new CompletableFuture<>(); vmService.getObject( getIsolateId(), instance.getId(), new GetObjectConsumer() { @Override public void onError(RPCError error) { future.completeExceptionally(new RuntimeException("RPCError calling getObject: " + error.toString())); } @Override public void received(Obj response) { future.complete((T)response); } @Override public void received(Sentinel response) { future.completeExceptionally(new RuntimeException("Sentinel calling getObject: " + response.toString())); } } ); return future; }); } @NotNull public CompletableFuture<XSourcePosition> getSourcePosition(DartVmServiceDebugProcess debugProcess, ScriptRef script, int tokenPos, InspectorService.ObjectGroup isAlive) { return addRequest(isAlive, "getSourcePosition", () -> CompletableFuture.completedFuture(debugProcess.getSourcePosition(isolateId, script, tokenPos))); } public CompletableFuture<Instance> getInstance(InstanceRef instance, InspectorService.ObjectGroup isAlive) { return getObjectHelper(instance, isAlive); } public CompletableFuture<Library> getLibrary(LibraryRef instance, InspectorService.ObjectGroup isAlive) { return getObjectHelper(instance, isAlive); } public CompletableFuture<ClassObj> getClass(ClassRef instance, InspectorService.ObjectGroup isAlive) { return getObjectHelper(instance, isAlive); } public CompletableFuture<Func> getFunc(FuncRef instance, InspectorService.ObjectGroup isAlive) { return getObjectHelper(instance, isAlive); } public CompletableFuture<Instance> getInstance(CompletableFuture<InstanceRef> instanceFuture, InspectorService.ObjectGroup isAlive) { return instanceFuture.thenComposeAsync((instance) -> getInstance(instance, isAlive)); } private JsonObject convertMapToJsonObject(Map<String, String> map) { final JsonObject obj = new JsonObject(); for (String key : map.keySet()) { obj.addProperty(key, map.get(key)); } return obj; } private void initialize(String isolateId) { this.isolateId = isolateId; vmService.getIsolate(isolateId, new GetIsolateConsumer() { @Override public void received(Isolate response) { for (LibraryRef library : response.getLibraries()) { if (libraryNames.contains(library.getUri())) { libraryRef.complete(library); return; } } libraryRef.completeExceptionally(new RuntimeException("No library matching " + libraryNames + " found.")); } @Override public void onError(RPCError error) { libraryRef.completeExceptionally(new RuntimeException("RPCError calling getIsolate:" + error.toString())); } @Override public void received(Sentinel response) { libraryRef.completeExceptionally(new RuntimeException("Sentinel calling getIsolate:" + response.toString())); } }); } }