blob: 2b4d5c10c6a5a3a5dfe8222c0a418b9615099c3a [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.
// @dart = 2.9
// 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/dwds.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
import 'package:pedantic/pedantic.dart';
import 'package:usage/uuid/uuid.dart';
import 'utilities.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);
String get frontendServerExecutable =>
p.join(dartSdkPath, 'bin', 'snapshots', 'frontend_server.dart.snapshot');
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();
}
bool compilerMessageReceived = false;
final CompilerMessageConsumer consumer;
String boundaryKey;
StdoutState state = StdoutState.collectDiagnostic;
Completer<CompilerOutput> compilerOutput;
final List<Uri> sources = <Uri>[];
bool _suppressCompilerMessages;
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;
}
var 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;
}
if (message.startsWith(boundaryKey)) {
if (_expectSources) {
if (state == StdoutState.collectDiagnostic) {
state = StdoutState.collectDependencies;
return;
}
}
if (message.length <= boundaryKey.length) {
compilerOutput.complete(null);
return;
}
var 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;
}
}
/// Converts filesystem paths to package URIs.
class PackageUriMapper {
PackageUriMapper(String scriptPath, String packagesFilePath,
String fileSystemScheme, List<String> fileSystemRoots) {
init(scriptPath, packagesFilePath, fileSystemScheme, fileSystemRoots);
}
Future<void> init(String scriptPath, String packagesFilePath,
String fileSystemScheme, List<String> fileSystemRoots) async {
var packageConfig = await loadPackageConfig(
File(fileSystem.path.absolute(packagesFilePath)));
var isWindowsPath =
Platform.isWindows && !scriptPath.startsWith('org-dartlang-app');
var scriptUri = Uri.file(scriptPath, windows: isWindowsPath).toString();
for (var package in packageConfig.packages) {
var prefix = package.packageUriRoot.toString();
// Only perform a multi-root mapping if there are multiple roots.
if (fileSystemScheme != null &&
fileSystemRoots != null &&
fileSystemRoots.length > 1 &&
prefix.contains(fileSystemScheme)) {
_packageName = package.name;
_uriPrefixes = fileSystemRoots
.map((String name) =>
Uri.file(name, windows: Platform.isWindows).toString())
.toList();
return;
}
if (scriptUri.startsWith(prefix)) {
_packageName = package.name;
_uriPrefixes = <String>[prefix];
return;
}
}
}
String _packageName;
List<String> _uriPrefixes;
Uri map(String scriptPath) {
if (_packageName == null) {
return null;
}
var scriptUri =
Uri.file(scriptPath, windows: Platform.isWindows).toString();
for (var uriPrefix in _uriPrefixes) {
if (scriptUri.startsWith(uriPrefix)) {
return Uri.parse(
'package:$_packageName/${scriptUri.substring(uriPrefix.length)}');
}
}
return null;
}
static Uri findUri(String scriptPath, String packagesFilePath,
String fileSystemScheme, List<String> fileSystemRoots) {
return PackageUriMapper(
scriptPath, packagesFilePath, fileSystemScheme, fileSystemRoots)
.map(scriptPath);
}
}
/// Class that allows to serialize compilation requests to the compiler.
abstract class _CompilationRequest {
_CompilationRequest(this.completer);
Completer<CompilerOutput> completer;
Future<CompilerOutput> _run(DefaultResidentCompiler compiler);
Future<void> run(DefaultResidentCompiler compiler) async {
completer.complete(await _run(compiler));
}
}
class _RecompileRequest extends _CompilationRequest {
_RecompileRequest(
Completer<CompilerOutput> completer,
this.mainPath,
this.invalidatedFiles,
this.outputPath,
this.packagesFilePath,
) : super(completer);
String mainPath;
List<Uri> invalidatedFiles;
String outputPath;
String packagesFilePath;
@override
Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
compiler._recompile(this);
}
class _CompileExpressionRequest extends _CompilationRequest {
_CompileExpressionRequest(
Completer<CompilerOutput> completer,
this.expression,
this.definitions,
this.typeDefinitions,
this.libraryUri,
this.klass,
this.isStatic,
) : super(completer);
String expression;
List<String> definitions;
List<String> typeDefinitions;
String libraryUri;
String klass;
bool isStatic;
@override
Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
compiler._compileExpression(this);
}
class _CompileExpressionToJsRequest extends _CompilationRequest {
_CompileExpressionToJsRequest(
Completer<CompilerOutput> completer,
this.libraryUri,
this.line,
this.column,
this.jsModules,
this.jsFrameValues,
this.moduleName,
this.expression)
: super(completer);
String libraryUri;
int line;
int column;
Map<String, String> jsModules;
Map<String, String> jsFrameValues;
String moduleName;
String expression;
@override
Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
compiler._compileExpressionToJs(this);
}
class _RejectRequest extends _CompilationRequest {
_RejectRequest(Completer<CompilerOutput> completer) : super(completer);
@override
Future<CompilerOutput> _run(DefaultResidentCompiler 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.
abstract class ResidentCompiler {
factory ResidentCompiler(
String sdkRoot, {
String packagesPath,
List<String> fileSystemRoots,
String fileSystemScheme,
String platformDill,
bool verbose,
CompilerMessageConsumer compilerMessageConsumer,
}) = DefaultResidentCompiler;
// TODO(jonahwilliams): find a better way to configure additional file system
// roots from the runner.
// See: https://github.com/flutter/flutter/issues/50494
void addFileSystemRoot(String root);
/// If invoked for the first time, it compiles Dart script identified by
/// [mainPath], [invalidatedFiles] list is ignored.
/// On successive runs [invalidatedFiles] indicates which files need to be
/// recompiled. If [mainPath] is null, previously used [mainPath] entry
/// point that is used for recompilation.
/// Binary file name is returned if compilation was successful, otherwise
/// null is returned.
Future<CompilerOutput> recompile(
String mainPath,
List<Uri> invalidatedFiles, {
@required String outputPath,
String packagesFilePath,
});
Future<CompilerOutput> compileExpression(
String expression,
List<String> definitions,
List<String> typeDefinitions,
String libraryUri,
String klass,
bool isStatic,
);
Future<CompilerOutput> compileExpressionToJs(
String libraryUri,
int line,
int column,
Map<String, String> jsModules,
Map<String, String> jsFrameValues,
String moduleName,
String expression);
/// 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();
/// 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();
/// 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();
/// stop the service normally
Future<dynamic> shutdown();
/// kill the service
Future<dynamic> kill();
}
@visibleForTesting
class DefaultResidentCompiler implements ResidentCompiler {
DefaultResidentCompiler(
String sdkRoot, {
this.packagesPath,
this.fileSystemRoots,
this.fileSystemScheme,
this.platformDill,
this.verbose,
CompilerMessageConsumer compilerMessageConsumer = defaultConsumer,
}) : assert(sdkRoot != null),
_stdoutHandler = StdoutHandler(consumer: compilerMessageConsumer),
// This is a URI, not a file path, so the forward slash is correct even on Windows.
sdkRoot = sdkRoot.endsWith('/') ? sdkRoot : '$sdkRoot/';
final String packagesPath;
final List<String> fileSystemRoots;
final String fileSystemScheme;
final String platformDill;
final bool verbose;
@override
void addFileSystemRoot(String root) {
fileSystemRoots.add(root);
}
/// 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>();
@override
Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles,
{@required String outputPath, String packagesFilePath}) async {
assert(outputPath != null);
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
var completer = Completer<CompilerOutput>();
_controller.add(_RecompileRequest(
completer, mainPath, invalidatedFiles, outputPath, packagesFilePath));
return completer.future;
}
Future<CompilerOutput> _recompile(_RecompileRequest request) async {
_stdoutHandler.reset();
// First time recompile is called we actually have to compile the app from
// scratch ignoring list of invalidated files.
PackageUriMapper packageUriMapper;
if (request.packagesFilePath != null) {
packageUriMapper = PackageUriMapper(
request.mainPath,
request.packagesFilePath,
fileSystemScheme,
fileSystemRoots,
);
}
_compileRequestNeedsConfirmation = true;
if (_server == null) {
return _compile(_mapFilename(request.mainPath, packageUriMapper),
request.outputPath, request.packagesFilePath);
}
var inputKey = Uuid().generateV4();
var mainUri = request.mainPath != null
? '${_mapFilename(request.mainPath, packageUriMapper)} '
: '';
_server.stdin.writeln('recompile $mainUri$inputKey');
_logger.info('<- recompile $mainUri$inputKey');
for (var fileUri in request.invalidatedFiles) {
var message = _mapFileUri(fileUri.toString(), packageUriMapper);
_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 {
var 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) {
var request = _compilationQueue.first;
await request.run(this);
_compilationQueue.removeAt(0);
}
}
}
Future<CompilerOutput> _compile(
String scriptUri, String outputFilePath, String packagesFilePath) async {
var frontendServer = frontendServerExecutable;
var args = <String>[
frontendServer,
'--sdk-root',
sdkRoot,
'--incremental',
'--target=dartdevc',
'-Ddart.developer.causal_async_stacks=true',
'--output-dill',
outputFilePath,
if (packagesFilePath != null) ...<String>[
'--packages',
packagesFilePath,
] else if (packagesPath != null) ...<String>[
'--packages',
packagesPath,
],
if (fileSystemRoots != null)
for (final String root in fileSystemRoots) ...<String>[
'--filesystem-root',
root,
],
if (fileSystemScheme != null) ...<String>[
'--filesystem-scheme',
fileSystemScheme,
],
if (platformDill != null) ...<String>[
'--platform',
platformDill,
],
'--debugger-module-names',
'--experimental-emit-debug-metadata',
if (verbose) '--verbose'
];
_logger.info(args.join(' '));
_server = await Process.start(Platform.resolvedExecutable, args,
workingDirectory: packagesPath);
_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;
}
@override
Future<CompilerOutput> compileExpression(
String expression,
List<String> definitions,
List<String> typeDefinitions,
String libraryUri,
String klass,
bool isStatic,
) {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
var completer = Completer<CompilerOutput>();
_controller.add(_CompileExpressionRequest(completer, expression,
definitions, typeDefinitions, libraryUri, 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;
}
var inputKey = Uuid().generateV4();
_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;
}
@override
Future<CompilerOutput> compileExpressionToJs(
String libraryUri,
int line,
int column,
Map<String, String> jsModules,
Map<String, String> jsFrameValues,
String moduleName,
String expression) {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
var completer = Completer<CompilerOutput>();
_controller.add(_CompileExpressionToJsRequest(completer, libraryUri, line,
column, jsModules, jsFrameValues, moduleName, expression));
return completer.future;
}
Future<CompilerOutput> _compileExpressionToJs(
_CompileExpressionToJsRequest request) async {
_stdoutHandler.reset(
suppressCompilerMessages: !verbose, expectSources: false);
// 'compile-expression-to-js' should be invoked after compiler has been started,
// program was compiled.
if (_server == null) {
return null;
}
var inputKey = Uuid().generateV4();
_server.stdin.writeln('compile-expression-to-js $inputKey');
_server.stdin.writeln(request.libraryUri ?? '');
_server.stdin.writeln(request.line);
_server.stdin.writeln(request.column);
request.jsModules?.forEach((k, v) {
_server.stdin.writeln('$k:$v');
});
_server.stdin.writeln(inputKey);
request.jsFrameValues?.forEach((k, v) {
_server.stdin.writeln('$k:$v');
});
_server.stdin.writeln(inputKey);
_server.stdin.writeln(request.moduleName ?? '');
_server.stdin.writeln(request.expression ?? '');
return _stdoutHandler.compilerOutput.future;
}
@override
void accept() {
if (_compileRequestNeedsConfirmation) {
_server.stdin.writeln('accept');
_logger.info('<- accept');
}
_compileRequestNeedsConfirmation = false;
}
@override
Future<CompilerOutput> reject() {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
var 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;
}
@override
void reset() {
_server?.stdin?.writeln('reset');
_logger.info('<- reset');
}
Future<int> quit() async {
_server.stdin.writeln('quit');
_logger.info('<- quit');
return _server.exitCode;
}
String _mapFilename(String filename, PackageUriMapper packageUriMapper) {
return _doMapFilename(filename, packageUriMapper) ?? filename;
}
String _mapFileUri(String fileUri, PackageUriMapper packageUriMapper) {
String filename;
try {
filename = Uri.parse(fileUri).toFilePath();
} on UnsupportedError catch (_) {
return fileUri;
}
return _doMapFilename(filename, packageUriMapper) ?? fileUri;
}
String _doMapFilename(String filename, PackageUriMapper packageUriMapper) {
if (packageUriMapper != null) {
var packageUri = packageUriMapper.map(filename);
if (packageUri != null) {
return packageUri.toString();
}
}
if (fileSystemRoots != null) {
for (var root in fileSystemRoots) {
if (filename.startsWith(root)) {
return Uri(
scheme: fileSystemScheme,
path: filename.substring(root.length))
.toString();
}
}
}
if (Platform.isWindows &&
fileSystemRoots != null &&
fileSystemRoots.length > 1) {
return Uri.file(filename, windows: Platform.isWindows).toString();
}
return null;
}
@override
Future<dynamic> shutdown() async {
// Server was never successfully created.
if (_server == null) {
return 0;
}
return quit();
}
@override
Future<dynamic> kill() async {
if (_server == null) {
return 0;
}
_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,
int line,
int column,
Map<String, String> jsModules,
Map<String, String> jsFrameValues,
String moduleName,
String expression) async {
var compilerOutput = await _generator.compileExpressionToJs(libraryUri,
line, column, jsModules, jsFrameValues, moduleName, expression);
if (compilerOutput != null && compilerOutput.outputFilename != null) {
var content = utf8.decode(
fileSystem.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({String moduleFormat, bool soundNullSafety}) async {}
}