blob: 954ae33b2903effdc25e4e83b101f2b993f5f489 [file] [log] [blame]
// Copyright 2018 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.
// This code is directly based on
// src/io/flutter/InspectorService.java
// If you add methods to this class you should also add them to
// InspectorService.java
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:vm_service/vm_service.dart';
import '../eval_on_dart_library.dart';
import '../globals.dart';
import 'diagnostics_node.dart';
import 'flutter_widget.dart';
// TODO(jacobr): remove flutter_web entry once flutter_web and flutter are
// unified.
const inspectorLibraryUriCandidates = [
'package:flutter/src/widgets/widget_inspector.dart',
'package:flutter_web/src/widgets/widget_inspector.dart',
];
bool _inspectorDependenciesLoaded = false;
// This method must be called before any methods on the Inspector are used.
Future<void> ensureInspectorServiceDependencies() async {
if (_inspectorDependenciesLoaded) {
return;
}
await Catalog.load();
// TODO(jacobr): consider also loading common icons needed by the inspector
// to avoid flicker on icon load.
_inspectorDependenciesLoaded = true;
}
/// Manages communication between inspector code running in the Flutter app and
/// the inspector.
class InspectorService {
InspectorService(
this.vmService,
this.inspectorLibrary,
this.supportedServiceMethods,
// ignore: prefer_collection_literals
) : clients = Set() {
vmService.onExtensionEvent.listen(onExtensionVmServiceRecieved);
vmService.onDebugEvent.listen(onDebugVmServiceReceived);
}
static int nextGroupId = 0;
final VmService vmService;
final Set<InspectorServiceClient> clients;
final EvalOnDartLibrary inspectorLibrary;
final Set<String> supportedServiceMethods;
/// [ensureInspectorServiceDependencies] must be called before this method is
/// called.
static Future<ObjectGroup> createGroup(
VmService vmService,
String groupName,
) async {
assert(_inspectorDependenciesLoaded);
return (await create(vmService)).createObjectGroup(groupName);
}
static Future<InspectorService> create(VmService vmService) async {
assert(_inspectorDependenciesLoaded);
assert(serviceManager.hasConnection);
assert(serviceManager.service != null);
final inspectorLibrary = EvalOnDartLibrary(
inspectorLibraryUriCandidates,
vmService,
);
final libraryRef = await inspectorLibrary.libraryRef.catchError(
(_) => throw FlutterInspectorLibraryNotFound(),
test: (e) => e is LibraryNotFound,
);
final libraryFuture = inspectorLibrary.getLibrary(libraryRef, null);
final library = await libraryFuture;
Future<Set<String>> lookupFunctionNames() async {
for (ClassRef classRef in library.classes) {
if ('WidgetInspectorService' == classRef.name) {
final classObj = await inspectorLibrary.getClass(classRef, null);
// ignore: prefer_collection_literals
final Set<String> functionNames = Set();
for (FuncRef funcRef in classObj.functions) {
functionNames.add(funcRef.name);
}
return functionNames;
}
}
// WidgetInspectorService is not available. Either this is not a Flutter
// application or it is running in profile mode.
return null;
}
final supportedServiceMethods = await lookupFunctionNames();
if (supportedServiceMethods == null) return null;
return InspectorService(
vmService,
inspectorLibrary,
supportedServiceMethods,
);
}
/// As we aren't running from an IDE, we don't know exactly what the pub root
/// directories are for the current project so we make a best guess if needed
/// based on the the root directory of the first non artifical widget in the
/// tree.
Future<String> inferPubRootDirectoryIfNeeded() async {
final group = createObjectGroup('temp');
final root = await group.getRoot(FlutterTreeType.widget);
if (root == null) {
// No need to do anything as there isn't a valid tree (yet?).
await group.dispose();
return null;
}
final children = await root.children;
if (children?.isNotEmpty == true) {
// There are already widgets identified as being from the summary tree so
// no need to guess the pub root directory.
return null;
}
final List<RemoteDiagnosticsNode> allChildren =
await group.getChildren(root.dartDiagnosticRef, false, null);
final path = allChildren.first.creationLocation?.path;
if (path == null) {
await group.dispose();
return null;
}
// TODO(jacobr): it would be nice to use Isolate.rootLib similar to how
// debugger.dart does but we are currently blocked by the
// --track-widget-creation transformer generating absolute paths instead of
// package:paths.
// Once https://github.com/flutter/flutter/issues/26615 is fixed we will be
// able to use package: paths. Temporarily all tools tracking widget
// locations will need to support both path formats.
// TODO(jacobr): use the list of loaded scripts to determine the appropriate
// package root directory given that the root script of this project is in
// this directory rather than guessing based on url structure.
final parts = path.split('/');
String pubRootDirectory;
for (int i = parts.length - 1; i >= 0; i--) {
String part;
if (part == 'lib' || part == 'web') {
pubRootDirectory = parts.sublist(0, i).join('/');
break;
}
if (part == 'packages') {
pubRootDirectory = parts.sublist(0, i + 1).join('/');
break;
}
}
pubRootDirectory ??= (parts..removeLast()).join('/');
await setPubRootDirectories([pubRootDirectory]);
await group.dispose();
return pubRootDirectory;
}
/// Returns whether to use the Daemon API or the VM Service protocol directly.
///
/// The VM Service protocol must be used when paused at a breakpoint as the
/// Daemon API calls won't execute until after the current frame is done
/// rendering.
bool get useDaemonApi {
return true;
// TODO(jacobr): once there is a debugger, hook to it to determine whether
// we are suspended.
// return !app.isFlutterIsolateSuspended();
}
/// Use this method to write code that is backwards compatible with versions
/// of Flutter that are too old to contain specific service methods.
bool hasServiceMethod(String methodName) {
return supportedServiceMethods.contains(methodName);
}
ObjectGroup createObjectGroup(String debugName) {
return ObjectGroup(debugName, this);
}
void dispose() {
inspectorLibrary.dispose();
}
Future<Object> forceRefresh() {
final List<Future<Object>> futures = [];
for (InspectorServiceClient client in clients) {
try {
futures.add(client.onForceRefresh());
} catch (e) {
log(e);
}
}
return Future.wait(futures);
}
void notifySelectionChanged() {
for (InspectorServiceClient client in clients) {
client.onInspectorSelectionChanged();
}
}
void addClient(InspectorServiceClient client) {
clients.add(client);
}
void onDebugVmServiceReceived(Event event) {
if (event.kind == EventKind.kInspect) {
// Make sure the WidgetInspector on the device switches to show the inspected object
// if the inspected object is a Widget or RenderObject.
// We create a dummy object group as this particular operation
// doesn't actually require an object group.
createObjectGroup('dummy').setSelection(event.inspectee, true);
// Update the UI in IntelliJ.
notifySelectionChanged();
}
}
void onExtensionVmServiceRecieved(Event e) {
if ('Flutter.Frame' == e.extensionKind) {
for (InspectorServiceClient client in clients) {
try {
client.onFlutterFrame();
} catch (e) {
log('Error handling frame event', error: e);
}
}
}
}
/// If the widget tree is not ready, the application should wait for the next
/// Flutter.Frame event before attempting to display the widget tree. If the
/// application is ready, the next Flutter.Frame event may never come as no
/// new frames will be triggered to draw unless something changes in the UI.
Future<bool> isWidgetTreeReady() {
return invokeBoolServiceMethodNoArgs('isWidgetTreeReady');
}
Future<bool> invokeBoolServiceMethodNoArgs(String methodName) async {
if (useDaemonApi) {
return await invokeServiceMethodDaemonNoGroupArgs(methodName) == true;
} else {
return (await invokeServiceMethodObservatoryNoGroup(methodName))
?.valueAsString ==
'true';
}
}
Future<bool> isWidgetCreationTracked() {
return invokeBoolServiceMethodNoArgs('isWidgetCreationTracked');
}
Future<Object> invokeServiceMethodDaemonNoGroupArgs(String methodName,
[List<String> args]) {
final Map<String, Object> params = {};
if (args != null) {
for (int i = 0; i < args.length; ++i) {
params['arg$i'] = args[i];
}
}
return invokeServiceMethodDaemonNoGroup(methodName, params);
}
Future<void> setPubRootDirectories(List<String> rootDirectories) {
// No need to call this from a breakpoint.
assert(useDaemonApi);
return invokeServiceMethodDaemonNoGroupArgs(
'setPubRootDirectories',
rootDirectories,
);
}
Future<InstanceRef> invokeServiceMethodObservatoryNoGroup(String methodName) {
return inspectorLibrary
.eval('WidgetInspectorService.instance.$methodName()', isAlive: null);
}
Future<Object> invokeServiceMethodDaemonNoGroup(
String methodName, Map<String, Object> args) async {
final r = await vmService.callServiceExtension(
'ext.flutter.inspector.$methodName',
isolateId: inspectorLibrary.isolateId,
args: args,
);
final json = r.json;
if (json['errorMessage'] != null) {
throw Exception('$methodName -- ${json['errorMessage']}');
}
return json['result'];
}
}
/// Class managing a group of inspector objects that can be freed by
/// a single call to dispose().
/// After dispose is called, all pending requests made with the ObjectGroup
/// will be skipped. This means that clients should not have to write any
/// special logic to handle orphaned requests.
class ObjectGroup {
ObjectGroup(
String debugName,
this.inspectorService,
) : groupName = '${debugName}_${InspectorService.nextGroupId}' {
InspectorService.nextGroupId++;
}
/// Object group all objects in this arena are allocated with.
final String groupName;
final InspectorService inspectorService;
bool disposed = false;
EvalOnDartLibrary get inspectorLibrary => inspectorService.inspectorLibrary;
bool get useDaemonApi => inspectorService.useDaemonApi;
/// Once an ObjectGroup has been disposed, all methods returning
/// DiagnosticsNode objects will return a placeholder dummy node and all methods
/// returning lists or maps will return empty lists and all other methods will
/// return null. Generally code should never call methods on a disposed object
/// group but sometimes due to chained futures that can be difficult to avoid
/// and it is simpler return an empty result that will be ignored anyway than to
/// attempt carefully cancel futures.
Future<void> dispose() {
final disposeComplete = invokeVoidServiceMethod('disposeGroup', groupName);
disposed = true;
return disposeComplete;
}
Future<T> nullIfDisposed<T>(Future<T> supplier()) async {
if (disposed) {
return null;
}
return await supplier();
}
T nullValueIfDisposed<T>(T supplier()) {
if (disposed) {
return null;
}
return supplier();
}
void skipIfDisposed(void runnable()) {
if (disposed) {
return;
}
runnable();
}
Future<SourcePosition> getPropertyLocation(
InstanceRef instanceRef, String name) async {
final Instance instance = await getInstance(instanceRef);
if (instance == null || disposed) {
return null;
}
return getPropertyLocationHelper(instance.classRef, name);
}
Future<SourcePosition> getPropertyLocationHelper(
ClassRef classRef, String name) async {
final clazz = await inspectorLibrary.getClass(classRef, this);
for (FuncRef f in clazz.functions) {
// TODO(pq): check for properties that match name.
if (f.name == name) {
final func = await inspectorLibrary.getFunc(f, this);
final SourceLocation location = func.location;
throw UnimplementedError(
'getSourcePosition not implemented. $location');
// return inspectorLibrary.getSourcePosition(
// debugProcess, location.script, location.tokenPos, this);
}
}
final ClassRef superClass = clazz.superClass;
return superClass == null
? null
: getPropertyLocationHelper(superClass, name);
}
Future<RemoteDiagnosticsNode> getRoot(FlutterTreeType type) {
// There is no excuse to call this method on a disposed group.
assert(!disposed);
switch (type) {
case FlutterTreeType.widget:
return getRootWidget();
case FlutterTreeType.renderObject:
return getRootRenderObject();
}
throw Exception('Unexpected FlutterTreeType');
}
/// Invokes a static method on the WidgetInspectorService class passing in the specified
/// arguments.
///
/// Intent is we could refactor how the API is invoked by only changing this call.
Future<InstanceRef> invokeServiceMethodObservatory(String methodName) {
return invokeServiceMethodObservatory1(methodName, groupName);
}
Future<InstanceRef> invokeServiceMethodObservatory1(
String methodName, String arg1) {
return inspectorLibrary.eval(
"WidgetInspectorService.instance.$methodName('$arg1')",
isAlive: this,
);
}
Future<Object> invokeServiceMethodDaemon(String methodName,
[String objectGroup]) {
return invokeServiceMethodDaemonParams(
methodName,
{'objectGroup': objectGroup ?? groupName},
);
}
Future<Object> invokeServiceMethodDaemonArg(
String methodName, String arg, String objectGroup) {
final args = {'objectGroup': objectGroup};
if (arg != null) {
args['arg'] = arg;
}
return invokeServiceMethodDaemonParams(methodName, args);
}
Future<Object> _callServiceExtension(
String extension, Map<String, Object> args) {
if (disposed) {
return Future.value(null);
}
return inspectorLibrary.addRequest(this, () async {
final r = await inspectorService.vmService.callServiceExtension(
extension,
isolateId: inspectorService.inspectorLibrary.isolateId,
args: args,
);
if (disposed) return null;
final json = r.json;
if (json['errorMessage'] != null) {
throw Exception('$extension -- ${json['errorMessage']}');
}
return json['result'];
});
}
// All calls to invokeServiceMethodDaemon bottom out to this call.
Future<Object> invokeServiceMethodDaemonParams(
String methodName,
Map<String, Object> params,
) {
return _callServiceExtension('ext.flutter.inspector.$methodName', params);
}
Future<Object> invokeServiceMethodDaemonInspectorRef(
String methodName, InspectorInstanceRef arg) {
return invokeServiceMethodDaemonArg(methodName, arg?.id, groupName);
}
Future<InstanceRef> invokeServiceMethodObservatoryInspectorRef(
String methodName, InspectorInstanceRef arg) {
return inspectorLibrary.eval(
"WidgetInspectorService.instance.$methodName('${arg?.id}', '$groupName')",
isAlive: this);
}
/// Call a service method passing in an observatory instance reference.
///
/// This call is useful when receiving an 'inspect' event from the
/// observatory and future use cases such as inspecting a Widget from a
/// log window.
///
/// This method will always need to use the observatory service as the input
/// parameter is an Observatory InstanceRef..
Future<InstanceRef> invokeServiceMethodOnRefObservatory(
String methodName, InstanceRef arg) {
if (arg == null) {
return inspectorLibrary.eval(
"WidgetInspectorService.instance.$methodName(null, '$groupName')",
isAlive: this,
);
}
return inspectorLibrary.eval(
"WidgetInspectorService.instance.$methodName(arg1, '$groupName')",
isAlive: this,
scope: {'arg1': arg.id},
);
}
Future<RemoteDiagnosticsNode> parseDiagnosticsNodeObservatory(
FutureOr<InstanceRef> instanceRefFuture) async {
return parseDiagnosticsNodeHelper(
await instanceRefToJson(await instanceRefFuture));
}
/// Returns a Future with a Map of property names to Observatory
/// InstanceRef objects. This method is shorthand for individually evaluating
/// each of the getters specified by property names.
///
/// It would be nice if the Observatory protocol provided a built in method
/// to get InstanceRef objects for a list of properties but this is
/// sufficient although slightly less efficient. The Observatory protocol
/// does provide fast access to all fields as part of an Instance object
/// but that is inadequate as for many Flutter data objects that we want
/// to display visually we care about properties that are not necessarily
/// fields.
///
/// The future will immediately complete to null if the inspectorInstanceRef is null.
Future<Map<String, InstanceRef>> getDartObjectProperties(
InspectorInstanceRef inspectorInstanceRef,
final List<String> propertyNames,
) async {
final instanceRef = await toObservatoryInstanceRef(inspectorInstanceRef);
if (disposed) return null;
const objectName = 'that';
final expression =
'[${propertyNames.map((propertyName) => '$objectName.$propertyName').join(',')}]';
final Map<String, String> scope = {objectName: instanceRef.id};
final instance = await getInstance(
inspectorLibrary.eval(expression, isAlive: this, scope: scope));
if (disposed) return null;
// We now have an instance object that is a Dart array of all the
// property values. Convert it back to a map from property name to
// property values.
final Map<String, InstanceRef> properties = {};
final List<InstanceRef> values = instance.elements.toList();
assert(values.length == propertyNames.length);
for (int i = 0; i < propertyNames.length; ++i) {
properties[propertyNames[i]] = values[i];
}
return properties;
}
Future<InstanceRef> toObservatoryInstanceRef(
InspectorInstanceRef inspectorInstanceRef) {
return invokeServiceMethodObservatoryInspectorRef(
'toObject', inspectorInstanceRef);
}
Future<Instance> getInstance(FutureOr<InstanceRef> instanceRef) async {
if (disposed) {
return null;
}
return inspectorLibrary.getInstance(await instanceRef, this);
}
Future<RemoteDiagnosticsNode> parseDiagnosticsNodeDaemon(
Future<Object> json) async {
if (disposed) return null;
return parseDiagnosticsNodeHelper(await json);
}
RemoteDiagnosticsNode parseDiagnosticsNodeHelper(
Map<String, Object> jsonElement) {
if (disposed) return null;
if (jsonElement == null) return null;
return RemoteDiagnosticsNode(jsonElement, this, false, null);
}
/// Requires that the InstanceRef is really referring to a String that is valid JSON.
Future<Object> instanceRefToJson(InstanceRef instanceRef) async {
if (disposed || instanceRef == null) return null;
final instance = await inspectorLibrary.getInstance(instanceRef, this);
if (disposed || instance == null) return null;
final String json = instance.valueAsString;
if (json == null) return null;
return jsonDecode(json);
}
Future<List<RemoteDiagnosticsNode>> parseDiagnosticsNodesObservatory(
FutureOr<InstanceRef> instanceRefFuture,
RemoteDiagnosticsNode parent) async {
if (disposed || instanceRefFuture == null) return [];
final instanceRef = await instanceRefFuture;
if (disposed || instanceRefFuture == null) return [];
return parseDiagnosticsNodesHelper(
await instanceRefToJson(instanceRef), parent);
}
List<RemoteDiagnosticsNode> parseDiagnosticsNodesHelper(
List<Object> jsonObject, RemoteDiagnosticsNode parent) {
if (disposed || jsonObject == null) return const [];
final List<RemoteDiagnosticsNode> nodes = [];
for (Map<String, Object> element in jsonObject) {
nodes.add(RemoteDiagnosticsNode(element, this, false, parent));
}
return nodes;
}
Future<List<RemoteDiagnosticsNode>> parseDiagnosticsNodesDaemon(
FutureOr<Object> jsonFuture, RemoteDiagnosticsNode parent) async {
if (disposed || jsonFuture == null) return const [];
return parseDiagnosticsNodesHelper(await jsonFuture, parent);
}
Future<List<RemoteDiagnosticsNode>> getChildren(
InspectorInstanceRef instanceRef,
bool summaryTree,
RemoteDiagnosticsNode parent,
) {
return getListHelper(
instanceRef,
summaryTree ? 'getChildrenSummaryTree' : 'getChildrenDetailsSubtree',
parent,
);
}
Future<List<RemoteDiagnosticsNode>> getProperties(
InspectorInstanceRef instanceRef) {
return getListHelper(instanceRef, 'getProperties', null);
}
Future<List<RemoteDiagnosticsNode>> getListHelper(
InspectorInstanceRef instanceRef,
String methodName,
RemoteDiagnosticsNode parent,
) async {
if (disposed) return const [];
if (useDaemonApi) {
return parseDiagnosticsNodesDaemon(
invokeServiceMethodDaemonInspectorRef(methodName, instanceRef),
parent);
} else {
return parseDiagnosticsNodesObservatory(
invokeServiceMethodObservatoryInspectorRef(methodName, instanceRef),
parent);
}
}
Future<RemoteDiagnosticsNode> invokeServiceMethodReturningNode(
String methodName) async {
if (disposed) return null;
if (useDaemonApi) {
return parseDiagnosticsNodeDaemon(invokeServiceMethodDaemon(methodName));
} else {
return parseDiagnosticsNodeObservatory(
invokeServiceMethodObservatory(methodName));
}
}
Future<RemoteDiagnosticsNode> invokeServiceMethodReturningNodeInspectorRef(
String methodName, InspectorInstanceRef ref) {
if (disposed) return null;
if (useDaemonApi) {
return parseDiagnosticsNodeDaemon(
invokeServiceMethodDaemonInspectorRef(methodName, ref));
} else {
return parseDiagnosticsNodeObservatory(
invokeServiceMethodObservatoryInspectorRef(methodName, ref));
}
}
Future<void> invokeVoidServiceMethod(String methodName, String arg1) async {
if (disposed) return;
if (useDaemonApi) {
await invokeServiceMethodDaemon(methodName, arg1);
} else {
await invokeServiceMethodObservatory1(methodName, arg1);
}
}
Future<void> invokeVoidServiceMethodInspectorRef(
String methodName, InspectorInstanceRef ref) async {
if (disposed) return;
if (useDaemonApi) {
await invokeServiceMethodDaemonInspectorRef(methodName, ref);
} else {
await invokeServiceMethodObservatoryInspectorRef(methodName, ref);
}
}
Future<RemoteDiagnosticsNode> getRootWidget() {
return invokeServiceMethodReturningNode('getRootWidgetSummaryTree');
}
Future<RemoteDiagnosticsNode> getRootWidgetFullTree() {
return invokeServiceMethodReturningNode('getRootWidget');
}
Future<RemoteDiagnosticsNode> getSummaryTreeWithoutIds() {
return parseDiagnosticsNodeDaemon(
invokeServiceMethodDaemon('getRootWidgetSummaryTree', null));
}
Future<RemoteDiagnosticsNode> getRootRenderObject() {
assert(!disposed);
return invokeServiceMethodReturningNode('getRootRenderObject');
}
/* TODO(jacobr): this probably isn't needed.
Future<List<DiagnosticsPathNode>> getParentChain(DiagnosticsNode target) async {
if (disposed) return null;
if (useDaemonApi) {
return parseDiagnosticsPathDaemon(invokeServiceMethodDaemon('getParentChain', target.getValueRef()));
}
else {
return parseDiagnosticsPathObservatory(invokeServiceMethodObservatory('getParentChain', target.getValueRef()));
}
});
}
Future<List<DiagnosticsPathNode>> parseDiagnosticsPathObservatory(Future<InstanceRef> instanceRefFuture) {
return nullIfDisposed(() -> instanceRefFuture.thenComposeAsync(this::parseDiagnosticsPathObservatory));
}
Future<List<DiagnosticsPathNode>> parseDiagnosticsPathObservatory(InstanceRef pathRef) {
return nullIfDisposed(() -> instanceRefToJson(pathRef).thenApplyAsync(this::parseDiagnosticsPathHelper));
}
Future<List<DiagnosticsPathNode>> parseDiagnosticsPathDaemon(Future<JsonElement> jsonFuture) {
return nullIfDisposed(() -> jsonFuture.thenApplyAsync(this::parseDiagnosticsPathHelper));
}
List<DiagnosticsPathNode> parseDiagnosticsPathHelper(JsonElement jsonElement) {
return nullValueIfDisposed(() -> {
final JsonArray jsonArray = jsonElement.getAsJsonArray();
final List<DiagnosticsPathNode> pathNodes = new List<>();
for (JsonElement element : jsonArray) {
pathNodes.add(new DiagnosticsPathNode(element.getAsJsonObject(), this));
}
return pathNodes;
});
}
*/
Future<RemoteDiagnosticsNode> getSelection(
RemoteDiagnosticsNode previousSelection,
FlutterTreeType treeType,
bool localOnly,
) async {
// There is no reason to allow calling this method on a disposed group.
assert(!disposed);
if (disposed) return null;
RemoteDiagnosticsNode newSelection;
final InspectorInstanceRef previousSelectionRef =
previousSelection != null ? previousSelection.dartDiagnosticRef : null;
switch (treeType) {
case FlutterTreeType.widget:
newSelection = await invokeServiceMethodReturningNodeInspectorRef(
localOnly ? 'getSelectedSummaryWidget' : 'getSelectedWidget',
previousSelectionRef);
break;
case FlutterTreeType.renderObject:
newSelection = await invokeServiceMethodReturningNodeInspectorRef(
'getSelectedRenderObject', previousSelectionRef);
break;
}
if (disposed) return null;
if (newSelection != null &&
newSelection.dartDiagnosticRef == previousSelectionRef) {
return previousSelection;
} else {
return newSelection;
}
}
Future<void> setSelectionInspector(
InspectorInstanceRef selection, bool uiAlreadyUpdated) {
if (disposed) {
return Future.value(null);
}
if (useDaemonApi) {
return handleSetSelectionDaemon(
invokeServiceMethodDaemonInspectorRef('setSelectionById', selection),
uiAlreadyUpdated);
} else {
return handleSetSelectionObservatory(
invokeServiceMethodObservatoryInspectorRef(
'setSelectionById', selection),
uiAlreadyUpdated);
}
}
/// Helper when we need to set selection given an observatory InstanceRef
/// instead of an InspectorInstanceRef.
void setSelection(InstanceRef selection, bool uiAlreadyUpdated) {
// There is no excuse for calling setSelection using a disposed ObjectGroup.
assert(!disposed);
// This call requires the observatory protocol as an observatory InstanceRef is specified.
handleSetSelectionObservatory(
invokeServiceMethodOnRefObservatory('setSelection', selection),
uiAlreadyUpdated);
}
Future<void> handleSetSelectionObservatory(
Future<InstanceRef> setSelectionResult, bool uiAlreadyUpdated) async {
// TODO(jacobr): we need to cancel if another inspect request comes in while we are trying this one.
if (disposed) return;
final instanceRef = await setSelectionResult;
if (disposed) return;
handleSetSelectionHelper(
'true' == instanceRef?.valueAsString, uiAlreadyUpdated);
}
void handleSetSelectionHelper(bool selectionChanged, bool uiAlreadyUpdated) {
if (selectionChanged && !uiAlreadyUpdated && !disposed) {
inspectorService.notifySelectionChanged();
}
}
Future<void> handleSetSelectionDaemon(
Future<Object> setSelectionResult, bool uiAlreadyUpdated) async {
if (disposed) return;
// TODO(jacobr): we need to cancel if another inspect request comes in while we are trying this one.
final json = await setSelectionResult;
if (disposed) return;
handleSetSelectionHelper(json, uiAlreadyUpdated);
}
Future<Map<String, InstanceRef>> getEnumPropertyValues(
InspectorInstanceRef ref) async {
if (disposed) return null;
if (ref?.id == null) return null;
final instance = await getInstance(await toObservatoryInstanceRef(ref));
if (disposed || instance == null) return null;
final clazz = await inspectorLibrary.getClass(instance.classRef, this);
if (disposed || clazz == null) return null;
final Map<String, InstanceRef> properties = {};
for (FieldRef field in clazz.fields) {
final String name = field.name;
if (name.startsWith('_')) {
// Needed to filter out _deleted_enum_sentinel synthetic property.
// If showing enum values is useful we could special case
// just the _deleted_enum_sentinel property name.
continue;
}
if (name == 'values') {
// Need to filter out the synthetic 'values' member.
// TODO(jacobr): detect that this properties return type is
// different and filter that way.
continue;
}
if (field.isConst && field.isStatic) {
properties[field.name] = field.declaredType;
}
}
return properties;
}
Future<RemoteDiagnosticsNode> getDetailsSubtree(
RemoteDiagnosticsNode node,
) async {
if (node == null) return null;
return invokeServiceMethodReturningNodeInspectorRef(
'getDetailsSubtree', node.dartDiagnosticRef);
}
}
enum FlutterTreeType {
widget, // ('Widget'),
renderObject // ('Render');
// TODO(jacobr): add semantics, and layer trees.
}
abstract class InspectorServiceClient {
void onInspectorSelectionChanged();
void onFlutterFrame();
Future<Object> onForceRefresh();
}
/// Reference to a Dart object.
///
/// This class is similar to the Observatory protocol InstanceRef with the
/// difference that InspectorInstanceRef objects do not expire and all
/// instances of the same Dart object are guaranteed to have the same
/// InspectorInstanceRef id. The tradeoff is the consumer of
/// InspectorInstanceRef objects is responsible for managing their lifecycles.
class InspectorInstanceRef {
const InspectorInstanceRef(this.id);
@override
bool operator ==(Object other) {
if (other is InspectorInstanceRef) {
return id == other.id;
}
return false;
}
@override
int get hashCode => id.hashCode;
@override
String toString() => 'instance-$id';
final String id;
}
/// Manager that simplifies preventing memory leaks when using the
/// InspectorService.
///
/// This class is designed for the use case where you want to manage
/// object references associated with the current displayed UI and object
/// references associated with the candidate next frame of UI to display. Once
/// the next frame is ready, you determine whether you want to display it and
/// discard the current frame and promote the next frame to the the current
/// frame if you want to display the next frame otherwise you discard the next
/// frame.
///
/// To use this class load all data you want for the next frame by using
/// the object group specified by [next] and then if you decide to switch
/// to display that frame, call promoteNext() otherwise call clearNext().
class InspectorObjectGroupManager {
InspectorObjectGroupManager(this.inspectorService, this.debugName);
final InspectorService inspectorService;
final String debugName;
ObjectGroup _current;
ObjectGroup _next;
Completer<void> _pendingNext;
Future<void> get pendingUpdateDone {
if (_pendingNext != null) {
return _pendingNext.future;
}
if (_next == null) {
// There is no pending update.
return Future.value(null);
}
_pendingNext = Completer();
return _pendingNext.future;
}
ObjectGroup get current {
_current ??= inspectorService.createObjectGroup(debugName);
return _current;
}
ObjectGroup get next {
_next ??= inspectorService.createObjectGroup(debugName);
return _next;
}
void clear(bool isolateStopped) {
if (isolateStopped) {
// The Dart VM will handle GCing the underlying memory.
_current = null;
_setNextNull();
} else {
clearCurrent();
cancelNext();
}
}
void promoteNext() {
clearCurrent();
_current = _next;
_setNextNull();
}
void clearCurrent() {
if (_current != null) {
_current.dispose();
_current = null;
}
}
void cancelNext() {
if (_next != null) {
_next.dispose();
_setNextNull();
}
}
void _setNextNull() {
_next = null;
if (_pendingNext != null) {
_pendingNext.complete(null);
_pendingNext = null;
}
}
}
class FlutterInspectorLibraryNotFound extends LibraryNotFound {
FlutterInspectorLibraryNotFound() : super(inspectorLibraryUriCandidates);
}