blob: f97d15d92dee91ece6f02c5d4bdf5c826c32b05b [file] [log] [blame]
// Copyright 2020 The Dart Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Note: this is a copy from flutter tools, updated to work with dwds tests
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dwds/expression_compiler.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart';
import 'package:test_common/test_sdk_layout.dart';
import 'utilities.dart';
import 'uuid.dart';
Logger _logger = Logger('FrontendServerClient');
Logger _serverLogger = Logger('FrontendServer');
void defaultConsumer(String message, {StackTrace? stackTrace}) =>
stackTrace == null
? _serverLogger.info(message)
: _serverLogger.severe(message, null, stackTrace);
typedef CompilerMessageConsumer =
void Function(String message, {StackTrace stackTrace});
class CompilerOutput {
const CompilerOutput(this.outputFilename, this.errorCount, this.sources);
final String outputFilename;
final int errorCount;
final List<Uri> sources;
}
enum StdoutState { collectDiagnostic, collectDependencies }
/// Handles stdin/stdout communication with the frontend server.
class StdoutHandler {
StdoutHandler({required this.consumer}) {
reset();
}
final CompilerMessageConsumer consumer;
late Completer<CompilerOutput?> compilerOutput;
final List<Uri> _sources = <Uri>[];
bool _compilerMessageReceived = false;
String? _boundaryKey;
StdoutState _state = StdoutState.collectDiagnostic;
late bool _suppressCompilerMessages;
late bool _expectSources;
bool _badState = false;
void handler(String message) {
if (message.startsWith('Observatory listening')) {
stderr.writeln(message);
return;
}
if (message.startsWith('Observatory server failed')) {
throw Exception(message);
}
if (_badState) {
return;
}
final kResultPrefix = 'result ';
if (_boundaryKey == null && message.startsWith(kResultPrefix)) {
_boundaryKey = message.substring(kResultPrefix.length);
return;
}
// Invalid state, see commented issue below for more information.
// NB: both the completeError and _badState flags are required to avoid
// filling the console with exceptions.
if (_boundaryKey == null) {
// Throwing a synchronous exception via throwToolExit will fail to cancel
// the stream. Instead use completeError so that the error is returned
// from the awaited future that the compiler consumers are expecting.
compilerOutput.completeError(
'Frontend server tests encountered an internal problem. '
'This can be caused by printing to stdout into the stream that is '
'used for communication between frontend server (in sdk) or '
'frontend server client (in dwds tests).'
'\n\n'
'Additional debugging information:\n'
' StdoutState: $_state\n'
' compilerMessageReceived: $_compilerMessageReceived\n'
' message: $message\n'
' _expectSources: $_expectSources\n'
' sources: $_sources\n',
);
// There are several event turns before the tool actually exits from a
// tool exception. Normally, the stream should be cancelled to prevent
// more events from entering the bad state, but because the error
// is coming from handler itself, there is no clean way to pipe this
// through. Instead, we set a flag to prevent more messages from
// registering.
_badState = true;
return;
}
final boundaryKey = _boundaryKey!;
if (message.startsWith(boundaryKey)) {
if (_expectSources) {
if (_state == StdoutState.collectDiagnostic) {
_state = StdoutState.collectDependencies;
return;
}
}
if (message.length <= boundaryKey.length) {
compilerOutput.complete(null);
return;
}
final spaceDelimiter = message.lastIndexOf(' ');
compilerOutput.complete(
CompilerOutput(
message.substring(boundaryKey.length + 1, spaceDelimiter),
int.parse(message.substring(spaceDelimiter + 1).trim()),
_sources,
),
);
return;
}
if (_state == StdoutState.collectDiagnostic) {
if (!_suppressCompilerMessages) {
if (_compilerMessageReceived == false) {
consumer('\nCompiler message:');
_compilerMessageReceived = true;
}
consumer(message);
}
} else {
assert(_state == StdoutState.collectDependencies);
switch (message[0]) {
case '+':
_sources.add(Uri.parse(message.substring(1)));
break;
case '-':
_sources.remove(Uri.parse(message.substring(1)));
break;
default:
_logger.warning('Unexpected prefix for $message uri - ignoring');
}
}
}
// This is needed to get ready to process next compilation result output,
// with its own boundary key and new completer.
void reset({
bool suppressCompilerMessages = false,
bool expectSources = true,
}) {
_boundaryKey = null;
_compilerMessageReceived = false;
compilerOutput = Completer<CompilerOutput?>();
_suppressCompilerMessages = suppressCompilerMessages;
_expectSources = expectSources;
_state = StdoutState.collectDiagnostic;
}
}
/// Class that allows to serialize compilation requests to the compiler.
abstract class _CompilationRequest {
_CompilationRequest(this.completer);
Completer<CompilerOutput?> completer;
Future<CompilerOutput?> _run(ResidentCompiler compiler);
Future<void> run(ResidentCompiler compiler) async {
completer.complete(await _run(compiler));
}
}
class _RecompileRequest extends _CompilationRequest {
_RecompileRequest(
super.completer,
this.mainUri,
this.invalidatedFiles,
this.outputPath,
this.packageConfig, {
required this.recompileRestart,
});
Uri mainUri;
List<Uri> invalidatedFiles;
String outputPath;
PackageConfig packageConfig;
bool recompileRestart;
@override
Future<CompilerOutput?> _run(ResidentCompiler compiler) async =>
compiler._recompile(this);
}
class _CompileExpressionRequest extends _CompilationRequest {
_CompileExpressionRequest(
super.completer,
this.expression,
this.definitions,
this.typeDefinitions,
this.libraryUri,
this.scriptUri,
this.klass,
this.isStatic,
);
String expression;
List<String> definitions;
List<String> typeDefinitions;
String? libraryUri;
String? scriptUri;
String? klass;
bool? isStatic;
@override
Future<CompilerOutput?> _run(ResidentCompiler compiler) async =>
compiler._compileExpression(this);
}
class _CompileExpressionToJsRequest extends _CompilationRequest {
_CompileExpressionToJsRequest(
super.completer,
this.libraryUri,
this.scriptUri,
this.line,
this.column,
this.jsModules,
this.jsFrameValues,
this.moduleName,
this.expression,
);
String libraryUri;
String scriptUri;
int line;
int column;
Map<String, String> jsModules;
Map<String, String> jsFrameValues;
String moduleName;
String expression;
@override
Future<CompilerOutput?> _run(ResidentCompiler compiler) async =>
compiler._compileExpressionToJs(this);
}
class _RejectRequest extends _CompilationRequest {
_RejectRequest(super.completer);
@override
Future<CompilerOutput?> _run(ResidentCompiler compiler) async =>
compiler._reject();
}
/// Wrapper around incremental frontend server compiler, that communicates with
/// server via stdin/stdout.
///
/// The wrapper is intended to stay resident in memory as user changes, reloads,
/// restarts the Flutter app.
class ResidentCompiler {
ResidentCompiler(
this.sdkRoot, {
required this.projectDirectory,
required this.packageConfigFile,
required this.useDebuggerModuleNames,
required this.fileSystemRoots,
required this.fileSystemScheme,
required this.platformDill,
required this.compilerOptions,
required this.sdkLayout,
this.verbose = false,
CompilerMessageConsumer compilerMessageConsumer = defaultConsumer,
}) : _stdoutHandler = StdoutHandler(consumer: compilerMessageConsumer);
final Uri projectDirectory;
final Uri packageConfigFile;
final bool useDebuggerModuleNames;
final List<Uri> fileSystemRoots;
final String fileSystemScheme;
final String platformDill;
final TestSdkLayout sdkLayout;
final CompilerOptions compilerOptions;
final bool verbose;
/// The path to the root of the Dart SDK used to compile.
final String sdkRoot;
Process? _server;
final StdoutHandler _stdoutHandler;
bool _compileRequestNeedsConfirmation = false;
final StreamController<_CompilationRequest> _controller =
StreamController<_CompilationRequest>();
/// If invoked for the first time, it compiles Dart script identified by
/// [mainUri], [invalidatedFiles] list is ignored.
/// On successive runs [invalidatedFiles] indicates which files need to be
/// recompiled. If [mainUri] is null, previously used [mainUri] entry
/// point that is used for recompilation.
/// Binary file name is returned if compilation was successful, otherwise
/// null is returned.
/// If [recompileRestart] is true, uses the `recompile-restart` instruction
/// instead of `recompile`.
Future<CompilerOutput?> recompile(
Uri mainUri,
List<Uri> invalidatedFiles, {
required String outputPath,
required PackageConfig packageConfig,
required bool recompileRestart,
}) async {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
final completer = Completer<CompilerOutput?>();
_controller.add(
_RecompileRequest(
completer,
mainUri,
invalidatedFiles,
outputPath,
packageConfig,
recompileRestart: recompileRestart,
),
);
return completer.future;
}
Future<CompilerOutput?> _recompile(_RecompileRequest request) async {
_stdoutHandler.reset();
final mainUri =
request.packageConfig.toPackageUri(request.mainUri)?.toString() ??
_toMultiRootPath(request.mainUri, fileSystemScheme, fileSystemRoots);
_compileRequestNeedsConfirmation = true;
if (_server == null) {
return _compile(mainUri, request.outputPath);
}
final server = _server!;
final inputKey = generateV4UUID();
final instruction = request.recompileRestart
? 'recompile-restart'
: 'recompile';
server.stdin.writeln('$instruction $mainUri $inputKey');
_logger.info('<- $instruction $mainUri $inputKey');
for (final fileUri in request.invalidatedFiles) {
String message;
if (fileUri.scheme == 'package') {
message = fileUri.toString();
} else {
message =
request.packageConfig.toPackageUri(fileUri)?.toString() ??
_toMultiRootPath(fileUri, fileSystemScheme, fileSystemRoots);
}
server.stdin.writeln(message);
_logger.info(message);
}
server.stdin.writeln(inputKey);
_logger.info('<- $inputKey');
return _stdoutHandler.compilerOutput.future;
}
final List<_CompilationRequest> _compilationQueue = <_CompilationRequest>[];
Future<void> _handleCompilationRequest(_CompilationRequest request) async {
final isEmpty = _compilationQueue.isEmpty;
_compilationQueue.add(request);
// Only trigger processing if queue was empty - i.e. no other requests
// are currently being processed. This effectively enforces "one
// compilation request at a time".
if (isEmpty) {
while (_compilationQueue.isNotEmpty) {
final request = _compilationQueue.first;
await request.run(this);
_compilationQueue.removeAt(0);
}
}
}
Future<CompilerOutput?> _compile(
String scriptUri,
String outputFilePath,
) async {
final frontendServer = sdkLayout.frontendServerSnapshotPath;
final args = <String>[
frontendServer,
'--sdk-root',
sdkRoot,
'--incremental',
'--target=dartdevc',
'-Ddart.developer.causal_async_stacks=true',
'--output-dill',
outputFilePath,
...<String>['--packages', '$packageConfigFile'],
for (final root in fileSystemRoots) ...<String>[
'--filesystem-root',
'$root',
],
...<String>['--filesystem-scheme', fileSystemScheme],
...<String>['--platform', platformDill],
if (useDebuggerModuleNames) '--debugger-module-names',
'--experimental-emit-debug-metadata',
for (final experiment in compilerOptions.experiments)
'--enable-experiment=$experiment',
if (compilerOptions.canaryFeatures) '--dartdevc-canary',
if (verbose) '--verbose',
if (compilerOptions.moduleFormat == ModuleFormat.ddc)
'--dartdevc-module-format=ddc',
];
_logger.info(args.join(' '));
final workingDirectory = projectDirectory.toFilePath();
_server = await Process.start(
sdkLayout.dartAotRuntimePath,
args,
workingDirectory: workingDirectory,
);
final server = _server!;
server.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(
_stdoutHandler.handler,
onDone: () {
// when outputFilename future is not completed, but stdout is closed
// process has died unexpectedly.
if (!_stdoutHandler.compilerOutput.isCompleted) {
_stdoutHandler.compilerOutput.complete(null);
throw Exception('the Dart compiler exited unexpectedly.');
}
},
);
server.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(_logger.info);
unawaited(
server.exitCode.then((int code) {
if (code != 0) {
throw Exception('the Dart compiler exited unexpectedly.');
}
}),
);
server.stdin.writeln('compile $scriptUri');
_logger.info('<- compile $scriptUri');
return _stdoutHandler.compilerOutput.future;
}
/// Compile dart expression to kernel.
Future<CompilerOutput?> compileExpression(
String expression,
List<String> definitions,
List<String> typeDefinitions,
String libraryUri,
String scriptUri,
String klass,
bool isStatic,
) {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
final completer = Completer<CompilerOutput?>();
_controller.add(
_CompileExpressionRequest(
completer,
expression,
definitions,
typeDefinitions,
libraryUri,
scriptUri,
klass,
isStatic,
),
);
return completer.future;
}
Future<CompilerOutput?> _compileExpression(
_CompileExpressionRequest request,
) async {
_stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false);
// 'compile-expression' should be invoked after compiler has been started,
// program was compiled.
if (_server == null) {
return null;
}
final server = _server!;
final inputKey = generateV4UUID();
server.stdin.writeln('compile-expression $inputKey');
server.stdin.writeln(request.expression);
request.definitions.forEach(server.stdin.writeln);
server.stdin.writeln(inputKey);
request.typeDefinitions.forEach(server.stdin.writeln);
server.stdin.writeln(inputKey);
server.stdin.writeln(request.libraryUri ?? '');
server.stdin.writeln(request.klass ?? '');
server.stdin.writeln(request.isStatic ?? false);
return _stdoutHandler.compilerOutput.future;
}
/// Compiles dart expression to JavaScript.
Future<CompilerOutput?> compileExpressionToJs(
String libraryUri,
String scriptUri,
int line,
int column,
Map<String, String> jsModules,
Map<String, String> jsFrameValues,
String moduleName,
String expression,
) {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
final completer = Completer<CompilerOutput?>();
_controller.add(
_CompileExpressionToJsRequest(
completer,
libraryUri,
scriptUri,
line,
column,
jsModules,
jsFrameValues,
moduleName,
expression,
),
);
return completer.future;
}
Future<CompilerOutput?> _compileExpressionToJs(
_CompileExpressionToJsRequest request,
) async {
_stdoutHandler.reset(
suppressCompilerMessages: !verbose,
expectSources: false,
);
// Compiling an expression should happen after the compiler has been
// started and the program was compiled.
if (_server == null) {
return null;
}
final server = _server!;
server.stdin.writeln('JSON_INPUT');
server.stdin.writeln(
json.encode({
'type': 'COMPILE_EXPRESSION_JS',
'data': {
'expression': request.expression,
'libraryUri': request.libraryUri,
'scriptUri': request.scriptUri,
'line': request.line,
'column': request.column,
'jsModules': request.jsModules,
'jsFrameValues': request.jsFrameValues,
'moduleName': request.moduleName,
},
}),
);
return _stdoutHandler.compilerOutput.future;
}
/// Should be invoked when results of compilation are accepted by the client.
///
/// Either [accept] or [reject] should be called after every [recompile] call.
void accept() {
if (_compileRequestNeedsConfirmation) {
_server!.stdin.writeln('accept');
_logger.info('<- accept');
}
_compileRequestNeedsConfirmation = false;
}
/// Should be invoked when results of compilation are rejected by the client.
///
/// Either [accept] or [reject] should be called after every [recompile] call.
Future<CompilerOutput?> reject() {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
final completer = Completer<CompilerOutput?>();
_controller.add(_RejectRequest(completer));
return completer.future;
}
Future<CompilerOutput?> _reject() {
if (!_compileRequestNeedsConfirmation) {
return Future<CompilerOutput?>.value(null);
}
_stdoutHandler.reset(expectSources: false);
_server!.stdin.writeln('reject');
_logger.info('<- reject');
_compileRequestNeedsConfirmation = false;
return _stdoutHandler.compilerOutput.future;
}
/// Should be invoked when frontend server compiler should forget what was
/// accepted previously so that next call to [recompile] produces complete
/// kernel file.
void reset() {
// TODO(annagrin): make sure this works when we support hot restart in
// tests using frontend server - for example, throw an error if the
// server is not available.
_server?.stdin.writeln('reset');
_logger.info('<- reset');
}
Future<int> quit() async {
_server?.stdin.writeln('quit');
_logger.info('<- quit');
if (_server == null) {
return 0;
}
return _server!.exitCode;
}
/// stop the service normally
Future<dynamic> shutdown() async {
// Server was never successfully created.
if (_server == null) {
return 0;
}
return quit();
}
/// kill the service
Future<dynamic> kill() async {
if (_server == null) {
return 0;
}
final server = _server!;
_logger.info('killing pid ${server.pid}');
server.kill();
return server.exitCode;
}
}
class TestExpressionCompiler implements ExpressionCompiler {
final ResidentCompiler _generator;
TestExpressionCompiler(this._generator);
@override
Future<ExpressionCompilationResult> compileExpressionToJs(
String isolateId,
String libraryUri,
String scriptUri,
int line,
int column,
Map<String, String> jsModules,
Map<String, String> jsFrameValues,
String moduleName,
String expression,
) async {
final compilerOutput = await _generator.compileExpressionToJs(
libraryUri,
scriptUri,
line,
column,
jsModules,
jsFrameValues,
moduleName,
expression,
);
if (compilerOutput != null) {
final content = utf8.decode(
localFileSystem.file(compilerOutput.outputFilename).readAsBytesSync(),
);
return ExpressionCompilationResult(
content,
compilerOutput.errorCount > 0,
);
}
throw Exception('Failed to compile $expression');
}
@override
Future<bool> updateDependencies(Map<String, ModuleInfo> modules) async =>
true;
@override
Future<void> initialize(CompilerOptions options) async {}
}
/// Convert a file URI into a multi-root scheme URI if provided, otherwise
/// return unmodified.
String _toMultiRootPath(
Uri fileUri,
String? scheme,
List<Uri> fileSystemRoots,
) {
if (scheme == null || fileSystemRoots.isEmpty || fileUri.scheme != 'file') {
return fileUri.toString();
}
final filePath = fileUri.toFilePath(windows: Platform.isWindows);
for (final fileSystemRoot in fileSystemRoots) {
final rootPath = fileSystemRoot.toFilePath(windows: Platform.isWindows);
if (filePath.startsWith(rootPath)) {
return '$scheme:///${filePath.substring(rootPath.length)}';
}
}
return fileUri.toString();
}