Skip to content

Commit 35e6bfa

Browse files
author
Anna Gringauze
authored
Support VMService.evaluate with scope (#1340)
* Support scope in ChromeProxyService.evaluate() Support evaluate with scope by - wrapping the expression in a function with scope keys as parameters - compiling the wrapper - and using chrome's Runtime.callFunctionOn to evaluate the compiled call with arguments Will be followed up with a cleanup of try/catch additions and removals. Closes: #1336 * Build * Minor fixes and rebuild * Addressed CR comments
1 parent 55308d2 commit 35e6bfa

File tree

5 files changed

+115
-16
lines changed

5 files changed

+115
-16
lines changed

dwds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Show lowered final fields using their original dart names.
77
- Limit simultaneous connections to asset server to prevent broken sockets.
88
- Fix hangs in hot restart.
9+
- Initial support for passing scope to `ChromeProxyService.evaluate`.
910

1011
## 11.1.1
1112

dwds/lib/src/debugging/inspector.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,25 @@ class AppInspector extends Domain {
208208
return RemoteObject(result.result['result'] as Map<String, Object>);
209209
}
210210

211+
/// Calls Chrome's Runtime.callFunctionOn method with a global function.
212+
///
213+
/// [evalExpression] should be a JS function definition that can accept
214+
/// [arguments].
215+
Future<RemoteObject> _jsCallFunction(
216+
String evalExpression, List<RemoteObject> arguments,
217+
{bool returnByValue = false}) async {
218+
var jsArguments = arguments.map(callArgumentFor).toList();
219+
var result =
220+
await remoteDebugger.sendCommand('Runtime.callFunctionOn', params: {
221+
'functionDeclaration': evalExpression,
222+
'arguments': jsArguments,
223+
'executionContextId': await contextId,
224+
'returnByValue': returnByValue,
225+
});
226+
handleErrorIfPresent(result, evalContents: evalExpression);
227+
return RemoteObject(result.result['result'] as Map<String, Object>);
228+
}
229+
211230
Future<RemoteObject> evaluate(
212231
String isolateId, String targetId, String expression,
213232
{Map<String, String> scope}) async {
@@ -313,6 +332,13 @@ function($argsString) {
313332
return _evaluateInLibrary(library, evalExpression, arguments);
314333
}
315334

335+
/// Call [function] with objects referred by [argumentIds] as arguments.
336+
Future<RemoteObject> callFunction(
337+
String function, Iterable<String> argumentIds) async {
338+
var arguments = argumentIds.map(remoteObjectFor).toList();
339+
return _jsCallFunction(function, arguments);
340+
}
341+
316342
Future<Library> getLibrary(String isolateId, String objectId) async {
317343
return await _getLibrary(isolateId, objectId);
318344
}

dwds/lib/src/services/chrome_proxy_service.dart

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,6 @@ class ChromeProxyService implements VmServiceInterface {
116116
_skipLists,
117117
uri,
118118
);
119-
_expressionEvaluator = _compiler == null
120-
? null
121-
: ExpressionEvaluator(debugger, _locations, _modules, _compiler);
122119
_debuggerCompleter.complete(debugger);
123120
}
124121

@@ -228,6 +225,10 @@ class ChromeProxyService implements VmServiceInterface {
228225
executionContext,
229226
);
230227

228+
_expressionEvaluator = _compiler == null
229+
? null
230+
: ExpressionEvaluator(_inspector, _locations, _modules, _compiler);
231+
231232
await debugger.reestablishBreakpoints(
232233
_previousBreakpoints, _disabledBreakpoints);
233234
_disabledBreakpoints.clear();
@@ -434,7 +435,7 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension(
434435
var library = await _inspector?.getLibrary(isolateId, targetId);
435436
var result = await _getEvaluationResult(
436437
() => _expressionEvaluator.evaluateExpression(
437-
isolateId, library.uri, expression),
438+
isolateId, library.uri, expression, scope),
438439
expression);
439440
if (result is ErrorRef) {
440441
error = result;
@@ -473,9 +474,19 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension(
473474
await isCompilerInitialized;
474475
_validateIsolateId(isolateId);
475476

477+
if (scope != null) {
478+
// TODO(annagrin): Implement scope support.
479+
// Issue: https://github.com/dart-lang/webdev/issues/1344
480+
throw RPCError(
481+
'evaluateInFrame',
482+
RPCError.kInvalidRequest,
483+
'Expression evaluation with scope is not supported '
484+
'for this configuration.');
485+
}
486+
476487
var result = await _getEvaluationResult(
477488
() => _expressionEvaluator.evaluateExpressionInFrame(
478-
isolateId, frameIndex, expression),
489+
isolateId, frameIndex, expression, scope),
479490
expression);
480491

481492
if (result is ErrorRef) {

dwds/lib/src/services/expression_evaluator.dart

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:logging/logging.dart';
88
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
99

1010
import '../debugging/dart_scope.dart';
11-
import '../debugging/debugger.dart';
11+
import '../debugging/inspector.dart';
1212
import '../debugging/location.dart';
1313
import '../debugging/modules.dart';
1414
import '../utilities/objects.dart' as chrome;
@@ -33,7 +33,7 @@ class ErrorKind {
3333
/// collect context for evaluation (scope, types, modules), and using
3434
/// ExpressionCompilerInterface to compile dart expressions to JavaScript.
3535
class ExpressionEvaluator {
36-
final Future<Debugger> _debugger;
36+
final AppInspector _inspector;
3737
final Locations _locations;
3838
final Modules _modules;
3939
final ExpressionCompiler _compiler;
@@ -43,7 +43,7 @@ class ExpressionEvaluator {
4343
RegExp('org-dartlang-debug:synthetic_debug_expression:.*:.*Error: ');
4444

4545
ExpressionEvaluator(
46-
this._debugger, this._locations, this._modules, this._compiler);
46+
this._inspector, this._locations, this._modules, this._compiler);
4747

4848
RemoteObject _createError(ErrorKind severity, String message) {
4949
return RemoteObject(
@@ -62,7 +62,11 @@ class ExpressionEvaluator {
6262
/// [libraryUri] dart library to evaluate the expression in.
6363
/// [expression] dart expression to evaluate.
6464
Future<RemoteObject> evaluateExpression(
65-
String isolateId, String libraryUri, String expression) async {
65+
String isolateId,
66+
String libraryUri,
67+
String expression,
68+
Map<String, String> scope,
69+
) async {
6670
if (_compiler == null) {
6771
return _createError(ErrorKind.internal,
6872
'ExpressionEvaluator needs an ExpressionCompiler');
@@ -74,6 +78,10 @@ class ExpressionEvaluator {
7478

7579
var module = await _modules.moduleForlibrary(libraryUri);
7680

81+
if (scope != null && scope.isNotEmpty) {
82+
var params = scope.keys.join(', ');
83+
expression = '($params) => $expression';
84+
}
7785
_logger.finest('Evaluating "$expression" at $module');
7886

7987
// Compile expression using an expression compiler, such as
@@ -88,8 +96,22 @@ class ExpressionEvaluator {
8896
}
8997

9098
// Send JS expression to chrome to evaluate.
91-
var result = await (await _debugger).evaluate(jsResult);
92-
result = _formatEvaluationError(result);
99+
RemoteObject result;
100+
if (scope != null && scope.isNotEmpty) {
101+
// Strip try/catch.
102+
// TODO: remove adding try/catch block in expression compiler.
103+
// https://github.com/dart-lang/webdev/issues/1341
104+
var lines = jsResult.split('\n');
105+
var inner = lines.getRange(2, lines.length - 3).join('\n');
106+
var function = 'function(t) {'
107+
' return $inner(t);'
108+
'}';
109+
result = await _inspector.callFunction(function, scope.values);
110+
result = _formatEvaluationError(result);
111+
} else {
112+
result = await _inspector.debugger.evaluate(jsResult);
113+
result = _formatEvaluationError(result);
114+
}
93115

94116
_logger.finest('Evaluated "$expression" to "$result"');
95117
return result;
@@ -107,8 +129,8 @@ class ExpressionEvaluator {
107129
/// [isolateId] current isolate ID.
108130
/// [frameIndex] JavaScript frame to evaluate the expression in.
109131
/// [expression] dart expression to evaluate.
110-
Future<RemoteObject> evaluateExpressionInFrame(
111-
String isolateId, int frameIndex, String expression) async {
132+
Future<RemoteObject> evaluateExpressionInFrame(String isolateId,
133+
int frameIndex, String expression, Map<String, String> scope) async {
112134
if (_compiler == null) {
113135
return _createError(ErrorKind.internal,
114136
'ExpressionEvaluator needs an ExpressionCompiler');
@@ -119,7 +141,7 @@ class ExpressionEvaluator {
119141
}
120142

121143
// Get JS scope and current JS location.
122-
var jsFrame = (await _debugger).jsFrameForIndex(frameIndex);
144+
var jsFrame = _inspector.debugger.jsFrameForIndex(frameIndex);
123145
if (jsFrame == null) {
124146
return _createError(
125147
ErrorKind.internal, 'No frame with index $frameIndex');
@@ -175,7 +197,7 @@ class ExpressionEvaluator {
175197
}
176198

177199
// Send JS expression to chrome to evaluate.
178-
var result = await (await _debugger)
200+
var result = await _inspector.debugger
179201
.evaluateJsOnCallFrameIndex(frameIndex, jsResult);
180202
result = _formatEvaluationError(result);
181203

@@ -271,7 +293,7 @@ class ExpressionEvaluator {
271293
var scopeChain = filterScopes(frame).reversed;
272294
for (var scope in scopeChain) {
273295
var scopeProperties =
274-
await (await _debugger).getProperties(scope.object.objectId);
296+
await _inspector.debugger.getProperties(scope.object.objectId);
275297

276298
collectVariables(scope.scope, scopeProperties);
277299
}

dwds/test/build_daemon_evaluate_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,27 @@ void main() async {
126126
await setup.service.resume(isolate.id);
127127
});
128128

129+
test('with scope override is not supported yet', () async {
130+
await onBreakPoint(isolate.id, mainScript, 'printLocal', () async {
131+
var event = await stream.firstWhere(
132+
(event) => event.kind == EventKind.kPauseBreakpoint);
133+
134+
var object = await setup.service.evaluateInFrame(
135+
isolate.id, event.topFrame.index, 'MainClass(0)');
136+
137+
var param = object as InstanceRef;
138+
139+
expect(
140+
() => setup.service.evaluateInFrame(
141+
isolate.id,
142+
event.topFrame.index,
143+
't.toString()',
144+
scope: {'t': param.id},
145+
),
146+
throwsRPCError);
147+
});
148+
});
149+
129150
test('local', () async {
130151
await onBreakPoint(isolate.id, mainScript, 'printLocal', () async {
131152
var event = await stream.firstWhere(
@@ -477,6 +498,24 @@ void main() async {
477498

478499
tearDown(() async {});
479500

501+
test('with scope override', () async {
502+
var library = isolate.rootLib;
503+
var object = await setup.service
504+
.evaluate(isolate.id, library.id, 'MainClass(0)');
505+
506+
var param = object as InstanceRef;
507+
var result = await setup.service.evaluate(
508+
isolate.id, library.id, 't.toString()',
509+
scope: {'t': param.id});
510+
511+
expect(
512+
result,
513+
const TypeMatcher<InstanceRef>().having(
514+
(instance) => instance.valueAsString,
515+
'valueAsString',
516+
'0'));
517+
});
518+
480519
test('uses symbol from the same library', () async {
481520
var library = isolate.rootLib;
482521
var result = await setup.service

0 commit comments

Comments
 (0)