Skip to content

Commit 7aad2b5

Browse files
authored
Add support for variables in stack frames (#479)
1 parent afede96 commit 7aad2b5

19 files changed

+599
-44
lines changed

dwds/lib/data/connect_request.g.dart

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/lib/data/devtools_request.g.dart

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/lib/data/isolate_events.g.dart

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/lib/data/run_request.g.dart

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/lib/src/chrome_proxy_service.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ const _stdoutTypes = ['log', 'info', 'warning'];
567567
/// result.
568568
void handleErrorIfPresent(WipResponse response,
569569
{String evalContents, Object additionalDetails}) {
570+
if (response == null) return;
570571
if (response.result.containsKey('exceptionDetails')) {
571572
throw ChromeDebugException(
572573
response.result['exceptionDetails'] as Map<String, dynamic>,

dwds/lib/src/debugger.dart

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'chrome_proxy_service.dart';
1111
import 'dart_uri.dart';
1212
import 'domain.dart';
1313
import 'location.dart';
14+
import 'objects.dart';
1415
import 'sources.dart';
1516

1617
/// Converts from ExceptionPauseMode strings to [PauseState] enums.
@@ -146,12 +147,13 @@ class Debugger extends Domain {
146147
sources = Sources(_assetHandler);
147148
// We must add a listener before enabling the debugger otherwise we will
148149
// miss events.
149-
_chromeDebugger.onScriptParsed.listen(sources.scriptParsed);
150-
_chromeDebugger.onPaused.listen(_pauseHandler);
151-
_chromeDebugger.onResumed.listen(_resumeHandler);
150+
// Allow a null debugger/connection for unit tests.
151+
_chromeDebugger?.onScriptParsed?.listen(sources.scriptParsed);
152+
_chromeDebugger?.onPaused?.listen(_pauseHandler);
153+
_chromeDebugger?.onResumed?.listen(_resumeHandler);
152154

153-
handleErrorIfPresent(await _tabConnection.page.enable() as WipResponse);
154-
handleErrorIfPresent(await _chromeDebugger.enable() as WipResponse);
155+
handleErrorIfPresent(await _tabConnection?.page?.enable() as WipResponse);
156+
handleErrorIfPresent(await _chromeDebugger?.enable() as WipResponse);
155157
}
156158

157159
/// Add a breakpoint at the given position.
@@ -268,10 +270,10 @@ class Debugger extends Domain {
268270

269271
/// Translates Chrome callFrames contained in [DebuggerPausedEvent] into Dart
270272
/// [Frame]s.
271-
List<Frame> _dartFramesFor(DebuggerPausedEvent e) {
273+
Future<List<Frame>> dartFramesFor(List<Map<String, dynamic>> frames) async {
272274
var dartFrames = <Frame>[];
273275
var index = 0;
274-
for (var frame in e.params['callFrames']) {
276+
for (var frame in frames) {
275277
var location = frame['location'];
276278
var functionName = frame['functionName'] as String ?? '';
277279
functionName = functionName.split('.').last;
@@ -282,12 +284,52 @@ class Debugger extends Domain {
282284
if (dartFrame != null) {
283285
dartFrame.code.name = functionName.isEmpty ? '<closure>' : functionName;
284286
dartFrame.index = index++;
287+
dartFrame.vars =
288+
await _variablesFor(frame['scopeChain'] as List<dynamic>);
285289
dartFrames.add(dartFrame);
286290
}
287291
}
288292
return dartFrames;
289293
}
290294

295+
/// The variables visible in a frame in Dart protocol [BoundVariable] form.
296+
Future<List<BoundVariable>> _variablesFor(List<dynamic> scopeChain) async {
297+
// TODO: Much better logic for which frames to use. This is probably just
298+
// the dynamically visible variables, so we should omit library scope.
299+
return [
300+
for (var scope in scopeChain.take(2)) ...await _boundVariables(scope)
301+
];
302+
}
303+
304+
/// The [BoundVariable]s visible in a v8 'scope' object as found in the
305+
/// 'scopeChain' field of the 'callFrames' in a DebuggerPausedEvent.
306+
Future<Iterable<BoundVariable>> _boundVariables(dynamic scope) async {
307+
var properties =
308+
await _getProperties(scope['object']['objectId'] as String);
309+
// We return one level of properties from this object. Sub-properties are
310+
// another round trip.
311+
var refs = properties
312+
.map<Future<BoundVariable>>((property) async => BoundVariable()
313+
..name = property.name
314+
..value = await inspector.instanceRefFor(property.value));
315+
return Future.wait(refs);
316+
}
317+
318+
/// Calls the Chrome Runtime.getProperties API for the object
319+
/// with [id].
320+
Future<List<Property>> _getProperties(String id) async {
321+
var response = await _tabConnection.runtime
322+
.sendCommand('Runtime.getProperties', params: {
323+
'objectId': id,
324+
'ownProperties': true,
325+
});
326+
var jsProperties = response.result['result'];
327+
var properties = (jsProperties as List)
328+
.map<Property>((each) => Property(each as Map<String, dynamic>))
329+
.toList();
330+
return properties;
331+
}
332+
291333
/// Returns a Dart [Frame] for a [JsLocation].
292334
Frame _frameFor(JsLocation jsLocation) {
293335
// TODO(sdk/issues/37240) - ideally we look for an exact location instead
@@ -332,7 +374,9 @@ class Debugger extends Domain {
332374
}
333375
event.kind = EventKind.kPauseInterrupted;
334376
}
335-
var frames = _dartFramesFor(e);
377+
var jsFrames =
378+
(e.params['callFrames'] as List).cast<Map<String, dynamic>>();
379+
var frames = await dartFramesFor(jsFrames);
336380
_pausedStack = Stack()
337381
..frames = frames
338382
..messages = [];

dwds/lib/src/domain.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ abstract class Domain {
1616

1717
Domain(this._appInspectorProvider);
1818

19+
/// A constructor for the AppInspector to call which doesn't set
20+
/// [_appInspectorProvider] as it's not used by the AppInspector.
21+
Domain.forInspector() : _appInspectorProvider = null;
22+
1923
AppInspector get inspector => _appInspectorProvider();
2024

2125
/// Validate that isolateId matches the current isolate we're connected to and

dwds/lib/src/inspector.dart

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.import 'dart:async';
44

5+
import 'dart:math' as math show min;
6+
57
import 'package:path/path.dart' as p;
68
import 'package:vm_service_lib/vm_service_lib.dart';
79
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
810

911
import 'chrome_proxy_service.dart';
1012
import 'dart_uri.dart';
1113
import 'debugger.dart';
14+
import 'domain.dart';
1215
import 'helpers.dart';
1316

1417
/// An inspector for a running Dart application contained in the
1518
/// [WipConnection].
1619
///
1720
/// Provides information about currently loaded scripts and objects and support
1821
/// for eval.
19-
class AppInspector {
22+
class AppInspector extends Domain {
2023
/// Map of class ID to [Class].
2124
final _classes = <String, Class>{};
2225

@@ -50,7 +53,13 @@ class AppInspector {
5053
this._assetHandler,
5154
this._debugger,
5255
this._root,
53-
) : isolateRef = _toIsolateRef(isolate);
56+
) : isolateRef = _toIsolateRef(isolate),
57+
super.forInspector();
58+
59+
@override
60+
61+
/// We are the inspector, so this getter is trivial.
62+
AppInspector get inspector => this;
5463

5564
Future<void> _initialize() async {
5665
isolate.libraries.addAll(await _getLibraryRefs());
@@ -146,9 +155,16 @@ function($argsString) {
146155
'scope': scope,
147156
});
148157
}
149-
var remoteObject =
150-
RemoteObject(result.result['result'] as Map<String, dynamic>);
158+
return await instanceRefFor(
159+
RemoteObject(result.result['result'] as Map<String, dynamic>));
160+
}
151161

162+
/// Create an [InstanceRef] for the given Chrome [remoteObject].
163+
Future<InstanceRef> instanceRefFor(RemoteObject remoteObject) async {
164+
// If we have a null result, treat it as a reference to null.
165+
if (remoteObject == null) {
166+
return _primitiveInstance(InstanceKind.kNull, remoteObject);
167+
}
152168
switch (remoteObject.type) {
153169
case 'string':
154170
return _primitiveInstance(InstanceKind.kString, remoteObject);
@@ -157,16 +173,27 @@ function($argsString) {
157173
case 'boolean':
158174
return _primitiveInstance(InstanceKind.kBool, remoteObject);
159175
case 'object':
176+
// TODO: Actual toString()
177+
var toString = 'Placeholder for toString() result';
178+
// TODO: Make the truncation consistent with the VM.
179+
var truncated = toString.substring(0, math.min(100, toString.length));
160180
return InstanceRef()
161181
..kind = InstanceKind.kPlainInstance
162182
..id = remoteObject.objectId
183+
..valueAsString = toString
184+
..valueAsStringIsTruncated = truncated.length != toString.length
163185
// TODO(jakemac): Create a real ClassRef, we need a way of looking
164186
// up the library for a given instance to create it though.
165187
// https://github.com/dart-lang/sdk/issues/36771.
166188
..classRef = ClassRef();
167189
default:
168-
throw UnsupportedError(
169-
'Unsupported response type ${remoteObject.type}');
190+
// Return unsupported types as a String placeholder for now.
191+
var unsupported = RemoteObject({
192+
'type': 'String',
193+
'value':
194+
'Unsupported type:${remoteObject.type} (${remoteObject.description})'
195+
});
196+
return _primitiveInstance(InstanceKind.kString, unsupported);
170197
}
171198
}
172199

@@ -189,7 +216,6 @@ function($argsString) {
189216
if (clazz != null) return clazz;
190217
var scriptRef = _scriptRefs[objectId];
191218
if (scriptRef != null) return await _getScript(isolateId, scriptRef);
192-
193219
throw UnsupportedError(
194220
'Only libraries and classes are supported for getObject');
195221
}
@@ -350,10 +376,7 @@ function($argsString) {
350376

351377
/// Returns all scripts in the isolate.
352378
Future<List<ScriptRef>> scriptRefs(String isolateId) async {
353-
if (isolateId != isolate.id) {
354-
throw ArgumentError.value(
355-
isolateId, 'isolateId', 'Unrecognized isolate id');
356-
}
379+
checkIsolate(isolateId);
357380
var scripts = <ScriptRef>[];
358381
for (var lib in isolate.libraries) {
359382
// We can't provide the source for `dart:` imports so ignore for now.

dwds/lib/src/objects.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.import 'dart:async';
4+
5+
/// A library for WebKit mirror objects and support code. These probably should
6+
/// get migrated into webkit_inspection_protocol over time.
7+
8+
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
9+
10+
/// Represents a property of an object.
11+
class Property {
12+
final Map<String, dynamic> _map;
13+
14+
Property(this._map);
15+
16+
/// The remote object value in unwrapped form.
17+
///
18+
/// Useful for getting access to properties of particular types of
19+
/// RemoteObject.
20+
Map<String, dynamic> get rawValue => _map['value'] as Map<String, dynamic>;
21+
22+
/// Remote object value in case of primitive values or JSON values (if it was
23+
/// requested). (optional)
24+
RemoteObject get value => RemoteObject(rawValue);
25+
26+
/// The name of the property
27+
String get name => _map['name'] as String;
28+
29+
@override
30+
String toString() => '$name $value';
31+
}

dwds/lib/src/sources.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,19 @@ class Sources {
6969
dartUri,
7070
);
7171
serverPaths.add(dartUri.serverPath);
72-
_sourceToLocation
73-
.putIfAbsent(dartUri.serverPath, () => Set())
74-
.add(location);
75-
_scriptIdToLocation
76-
.putIfAbsent(script.scriptId, () => Set())
77-
.add(location);
72+
noteLocation(dartUri.serverPath, location, script.scriptId);
7873
}
7974
}
8075
}
8176
}
8277

78+
/// Add [location] to our lookups for both the Dart and JS scripts.
79+
void noteLocation(
80+
String dartServerPath, Location location, String wipScriptId) {
81+
_sourceToLocation.putIfAbsent(dartServerPath, () => Set()).add(location);
82+
_scriptIdToLocation.putIfAbsent(wipScriptId, () => Set()).add(location);
83+
}
84+
8385
/// Returns the tokenPosTable for the provided Dart script path as defined
8486
/// in:
8587
/// https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script

0 commit comments

Comments
 (0)