Add compile-expression service. (#16161)
This registers compile-expression service that is used by dart vm to evaluate expressions when debugging flutter applications.
diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart
index 396aa98..e639f2c 100644
--- a/packages/flutter_tools/lib/src/compile.dart
+++ b/packages/flutter_tools/lib/src/compile.dart
@@ -299,6 +299,28 @@
return stdoutHandler.compilerOutput.future;
}
+ Future<CompilerOutput> compileExpression(String expression, List<String> definitions,
+ List<String> typeDefinitions, String libraryUri, String klass, bool isStatic) {
+ stdoutHandler.reset();
+
+ // 'compile-expression' should be invoked after compiler has been started,
+ // program was compiled.
+ if (_server == null)
+ return null;
+
+ final String inputKey = new Uuid().generateV4();
+ _server.stdin.writeln('compile-expression $inputKey');
+ _server.stdin.writeln(expression);
+ definitions?.forEach(_server.stdin.writeln);
+ _server.stdin.writeln(inputKey);
+ typeDefinitions?.forEach(_server.stdin.writeln);
+ _server.stdin.writeln(inputKey);
+ _server.stdin.writeln(libraryUri ?? '');
+ _server.stdin.writeln(klass ?? '');
+ _server.stdin.writeln(isStatic ?? false);
+
+ return stdoutHandler.compilerOutput.future;
+ }
/// Should be invoked when results of compilation are accepted by the client.
///
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 5265114..f6da760 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -64,16 +64,19 @@
/// The 'reloadSources' service can be used by other Service Protocol clients
/// connected to the VM (e.g. Observatory) to request a reload of the source
/// code of the running application (a.k.a. HotReload).
+ /// The 'compileExpression' service can be used to compile user-provided
+ /// expressions requested during debugging of the application.
/// This ensures that the reload process follows the normal orchestration of
/// the Flutter Tools and not just the VM internal service.
- Future<Null> _connect({ReloadSources reloadSources}) async {
+ Future<Null> _connect({ReloadSources reloadSources, CompileExpression compileExpression}) async {
if (vmServices != null)
return;
vmServices = new List<VMService>(observatoryUris.length);
for (int i = 0; i < observatoryUris.length; i++) {
printTrace('Connecting to service protocol: ${observatoryUris[i]}');
vmServices[i] = await VMService.connect(observatoryUris[i],
- reloadSources: reloadSources);
+ reloadSources: reloadSources,
+ compileExpression: compileExpression);
printTrace('Successfully connected to service protocol: ${observatoryUris[i]}');
}
}
@@ -622,14 +625,15 @@
/// If the [reloadSources] parameter is not null the 'reloadSources' service
/// will be registered
Future<Null> connectToServiceProtocol({String viewFilter,
- ReloadSources reloadSources}) async {
+ ReloadSources reloadSources, CompileExpression compileExpression}) async {
if (!debuggingOptions.debuggingEnabled)
return new Future<Null>.error('Error the service protocol is not enabled.');
bool viewFound = false;
for (FlutterDevice device in flutterDevices) {
device.viewFilter = viewFilter;
- await device._connect(reloadSources: reloadSources);
+ await device._connect(reloadSources: reloadSources,
+ compileExpression: compileExpression);
await device.getVMs();
await device.waitForViews();
if (device.views == null)
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 77be01f..9eeb8df 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
@@ -13,6 +14,7 @@
import 'base/logger.dart';
import 'base/utils.dart';
import 'build_info.dart';
+import 'compile.dart';
import 'dart/dependencies.dart';
import 'device.dart';
import 'globals.dart';
@@ -105,6 +107,23 @@
}
}
+ Future<String> _compileExpressionService(String isolateId, String expression,
+ List<String> definitions, List<String> typeDefinitions,
+ String libraryUri, String klass, bool isStatic,
+ ) async {
+ for (FlutterDevice device in flutterDevices) {
+ if (device.generator != null) {
+ final CompilerOutput compilerOutput =
+ await device.generator.compileExpression(expression, definitions,
+ typeDefinitions, libraryUri, klass, isStatic);
+ if (compilerOutput.outputFilename != null) {
+ return base64.encode(fs.file(compilerOutput.outputFilename).readAsBytesSync());
+ }
+ }
+ }
+ return null;
+ }
+
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<Null> appStartedCompleter,
@@ -112,7 +131,8 @@
}) async {
try {
await connectToServiceProtocol(viewFilter: viewFilter,
- reloadSources: _reloadSourcesService);
+ reloadSources: _reloadSourcesService,
+ compileExpression: _compileExpressionService);
} catch (error) {
printError('Error connecting to the service protocol: $error');
return 2;
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 88420de..ef6210d 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -42,6 +42,16 @@
bool pause,
});
+typedef Future<String> CompileExpression(
+ String isolateId,
+ String expression,
+ List<String> definitions,
+ List<String> typeDefinitions,
+ String libraryUri,
+ String klass,
+ bool isStatic,
+);
+
const String _kRecordingType = 'vmservice';
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri) async {
@@ -99,6 +109,7 @@
this.wsAddress,
this._requestTimeout,
ReloadSources reloadSources,
+ CompileExpression compileExpression,
) {
_vm = new VM._empty(this);
_peer.listen().catchError(_connectionError.completeError);
@@ -138,6 +149,42 @@
'alias': 'Flutter Tools'
});
}
+
+ if (compileExpression != null) {
+ _peer.registerMethod('compileExpression', (rpc.Parameters params) async {
+ final String isolateId = params['isolateId'].asString;
+ if (isolateId is! String || isolateId.isEmpty)
+ throw new rpc.RpcException.invalidParams(
+ 'Invalid \'isolateId\': $isolateId');
+ final String expression = params['expression'].asString;
+ if (expression is! String || expression.isEmpty)
+ throw new rpc.RpcException.invalidParams(
+ 'Invalid \'expression\': $expression');
+ final List<String> definitions = params['definitions'].asList;
+ final List<String> typeDefinitions = params['typeDefinitions'].asList;
+ final String libraryUri = params['libraryUri'].asString;
+ final String klass = params['klass'] != null ? params['klass'].asString : null;
+ final bool isStatic = params['isStatic'].asBoolOr(false);
+
+ try {
+ final String kernelBytesBase64 = await compileExpression(isolateId,
+ expression, definitions, typeDefinitions, libraryUri, klass,
+ isStatic);
+ return <String, dynamic>{'type': 'Success',
+ 'result': <String, dynamic> {'kernelBytes': kernelBytesBase64}};
+ } on rpc.RpcException {
+ rethrow;
+ } catch (e, st) {
+ throw new rpc.RpcException(rpc_error_code.SERVER_ERROR,
+ 'Error during expression compilation: $e\n$st');
+ }
+ });
+
+ _peer.sendNotification('_registerService', <String, String>{
+ 'service': 'compileExpression',
+ 'alias': 'Flutter Tools'
+ });
+ }
}
/// Enables recording of VMService JSON-rpc activity to the specified base
@@ -180,11 +227,12 @@
Uri httpUri, {
Duration requestTimeout = kDefaultRequestTimeout,
ReloadSources reloadSources,
+ CompileExpression compileExpression,
}) async {
final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
final StreamChannel<String> channel = await _openChannel(wsUri);
final rpc.Peer peer = new rpc.Peer.withoutJson(jsonDocument.bind(channel));
- final VMService service = new VMService._(peer, httpUri, wsUri, requestTimeout, reloadSources);
+ final VMService service = new VMService._(peer, httpUri, wsUri, requestTimeout, reloadSources, compileExpression);
// This call is to ensure we are able to establish a connection instead of
// keeping on trucking and failing farther down the process.
await service._sendRequest('getVersion', const <String, dynamic>{});
diff --git a/packages/flutter_tools/test/compile_test.dart b/packages/flutter_tools/test/compile_test.dart
index 2fe63a8..61f1208 100644
--- a/packages/flutter_tools/test/compile_test.dart
+++ b/packages/flutter_tools/test/compile_test.dart
@@ -216,6 +216,89 @@
ProcessManager: () => mockProcessManager,
});
});
+
+ group('compile expression', ()
+ {
+ ProcessManager mockProcessManager;
+ ResidentCompiler generator;
+ MockProcess mockFrontendServer;
+ MockStdIn mockFrontendServerStdIn;
+ MockStream mockFrontendServerStdErr;
+ StreamController<String> stdErrStreamController;
+
+ setUp(() {
+ generator = new ResidentCompiler('sdkroot');
+ mockProcessManager = new MockProcessManager();
+ mockFrontendServer = new MockProcess();
+ mockFrontendServerStdIn = new MockStdIn();
+ mockFrontendServerStdErr = new MockStream();
+
+ when(mockFrontendServer.stdin).thenReturn(mockFrontendServerStdIn);
+ when(mockFrontendServer.stderr)
+ .thenAnswer((Invocation invocation) => mockFrontendServerStdErr);
+ stdErrStreamController = new StreamController<String>();
+ when(mockFrontendServerStdErr.transform<String>(any))
+ .thenAnswer((Invocation invocation) => stdErrStreamController.stream);
+
+ when(mockProcessManager.canRun(any)).thenReturn(true);
+ when(mockProcessManager.start(any)).thenAnswer(
+ (Invocation invocation) =>
+ new Future<Process>.value(mockFrontendServer)
+ );
+ });
+
+ tearDown(() {
+ verifyNever(mockFrontendServer.exitCode);
+ });
+
+ testUsingContext('fails if not previously compiled', () async {
+ final CompilerOutput result = await generator.compileExpression(
+ '2+2', null, null, null, null, false);
+ expect(result, isNull);
+ });
+
+ testUsingContext('compile single expression', () async {
+ final BufferLogger logger = context[Logger];
+
+ final Completer<List<int>> compileResponseCompleter =
+ new Completer<List<int>>();
+ final Completer<List<int>> compileExpressionResponseCompleter =
+ new Completer<List<int>>();
+
+ when(mockFrontendServer.stdout)
+ .thenAnswer((Invocation invocation) =>
+ new Stream<List<int>>.fromFutures(
+ <Future<List<int>>>[
+ compileResponseCompleter.future,
+ compileExpressionResponseCompleter.future]));
+
+ compileResponseCompleter.complete(new Future<List<int>>.value(utf8.encode(
+ 'result abc\nline1\nline2\nabc /path/to/main.dart.dill 0\n'
+ )));
+
+ final CompilerOutput output = await generator.recompile(
+ '/path/to/main.dart', null /* invalidatedFiles */
+ );
+ expect(mockFrontendServerStdIn.getAndClear(),
+ 'compile /path/to/main.dart\n');
+ verifyNoMoreInteractions(mockFrontendServerStdIn);
+ expect(logger.errorText,
+ equals('compiler message: line1\ncompiler message: line2\n'));
+ expect(output.outputFilename, equals('/path/to/main.dart.dill'));
+
+ compileExpressionResponseCompleter.complete(new Future<List<int>>.value(utf8.encode(
+ 'result def\nline1\nline2\ndef /path/to/main.dart.dill.incremental 0\n'
+ )));
+
+ final CompilerOutput outputExpression = await generator.compileExpression(
+ '2+2', null, null, null, null, false);
+ expect(outputExpression, isNotNull);
+ expect(outputExpression.outputFilename, equals('/path/to/main.dart.dill.incremental'));
+ expect(outputExpression.errorCount, 0);
+ }, overrides: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ });
+ });
}
Future<Null> _recompile(StreamController<List<int>> streamController,