blob: 1ffb4fb69e2909627b3908af53145d18636bcd8b [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. 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
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:async/async.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';
import '../utilities/dart_uri.dart';
import 'expression_compiler.dart';
final _logger = Logger('ExpressionCompilerService');
class _Compiler {
final Isolate _worker;
final StreamQueue<Object> _responseQueue;
final ReceivePort _receivePort;
final SendPort _sendPort;
Future<void> _dependencyUpdate;
_Compiler._(
this._worker,
this._responseQueue,
this._receivePort,
this._sendPort,
);
/// Sends [request] on [_sendPort] and returns the next event from the
/// response stream.
Future<Map<String, Object>> _send(Map<String, Object> request) async {
_sendPort.send(request);
return await _responseQueue.hasNext
? await _responseQueue.next as Map<String, Object>
: {
'succeeded': false,
'errors': ['compilation service response stream closed'],
};
}
/// Starts expression compilation service.
///
/// Starts expression compiler worker in an isolate and creates the
/// expression compilation service that communicates to the worker.
///
/// [workerPath] is the path for the expression compiler worker,
/// [sdkSummaryPath] is the path to the sdk summary dill file
/// [librariesPath] is the path to libraries definitions file
/// libraries.json.
///
/// [soundNullSafety] indiciates if the compioler should support sound null
/// safety.
///
/// Performs handshake with the isolate running expression compiler
/// worker to estabish communication via send/receive ports, returns
/// the service after the communication is established.
///
/// Users need to stop the service by calling [stop].
static Future<_Compiler> start(
String address,
int port,
String workerPath,
String sdkSummaryPath,
String librariesPath,
String moduleFormat,
bool verbose,
bool soundNullSafety,
) async {
final workerUri = p.toUri(p.absolute(workerPath));
if (!File.fromUri(workerUri).existsSync()) {
_logger.severe('Worker path $workerPath does not exist');
}
if (!File(sdkSummaryPath).existsSync()) {
_logger.severe('SDK summary path $sdkSummaryPath does not exist');
}
if (!File(librariesPath).existsSync()) {
_logger.severe('Libraries path $librariesPath does not exist');
}
final sdkSummaryUri = Uri.file(sdkSummaryPath);
final librariesUri = Uri.file(librariesPath);
final args = [
'--experimental-expression-compiler',
'--libraries-file',
'$librariesUri',
'--dart-sdk-summary',
'$sdkSummaryUri',
'--asset-server-address',
address,
'--asset-server-port',
'$port',
'--module-format',
moduleFormat,
if (verbose) '--verbose',
soundNullSafety ? '--sound-null-safety' : '--no-sound-null-safety',
];
_logger.info('Starting...');
_logger.finest('$workerUri ${args.join(' ')}');
final receivePort = ReceivePort();
final isolate = await Isolate.spawnUri(
workerUri,
args,
receivePort.sendPort,
// Note(annagrin): ddc snapshot is generated with no asserts, so we have
// to run it unchecked in case the calling isolate is checked, as it
// happens, for example, when debugging webdev in VSCode or running tests
// using 'pub run'
checked: false,
);
var responseQueue = StreamQueue(receivePort);
var sendPort = await responseQueue.next as SendPort;
var service = _Compiler._(isolate, responseQueue, receivePort, sendPort);
return service;
}
Future<bool> updateDependencies(Map<String, ModuleInfo> modules) async {
if (_worker == null) {
throw StateError('Expression compilation service has stopped');
}
var updateCompleter = Completer();
_dependencyUpdate = updateCompleter.future;
_logger.info('Updating dependencies...');
_logger.finest('Dependencies: $modules');
var response = await _send({
'command': 'UpdateDeps',
'inputs': [
for (var moduleName in modules.keys)
{
'path': modules[moduleName].fullDillPath,
if (modules[moduleName].summaryPath != null)
'summaryPath': modules[moduleName].summaryPath,
'moduleName': moduleName
},
]
});
var result = response == null ? false : response['succeeded'] as bool;
if (result) {
_logger.info('Updated dependencies.');
} else {
var e = response['exception'];
var s = response['stackTrace'];
_logger.severe('Failed to update dependencies: $e:$s');
}
updateCompleter.complete();
return result;
}
Future<ExpressionCompilationResult> compileExpressionToJs(
String isolateId,
String libraryUri,
int line,
int column,
Map<String, String> jsModules,
Map<String, String> jsFrameValues,
String moduleName,
String expression,
) async {
if (_worker == null) {
throw StateError('Expression compilation service has stopped');
}
_logger.finest('Waiting for dependencies to update');
await _dependencyUpdate;
_logger.finest('Compiling "$expression" at $libraryUri:$line');
var response = await _send({
'command': 'CompileExpression',
'expression': expression,
'line': line,
'column': column,
'jsModules': jsModules,
'jsScope': jsFrameValues,
'libraryUri': libraryUri,
'moduleName': moduleName,
});
var succeeded = false;
var result = '<unknown error>';
if (response != null) {
var errors = response['errors'] as List<String>;
var e = response['exception'];
var s = response['stackTrace'];
var error = (errors != null && errors.isNotEmpty)
? errors.first
: (e != null ? '$e:$s' : '<unknown error>');
var procedure = response['compiledProcedure'] as String;
succeeded = response['succeeded'] as bool;
result = succeeded ? procedure : error;
}
if (succeeded) {
_logger.finest('Compiled "$expression" to: $result');
} else {
_logger.finest('Failed to compile "$expression": $result');
}
return ExpressionCompilationResult(result, !succeeded);
}
/// Stops the service.
///
/// Terminates the isolate running expression compiler worker
/// and marks the service as stopped.
Future<void> stop() async {
_sendPort.send({'command': 'Shutdown'});
_receivePort.close();
_logger.info('Stopped.');
}
}
/// Service that handles expression compilation requests.
///
/// Expression compiler service spawns a dartdevc in expression compilation
/// mode in an isolate and communicates with the isolate via send/receive
/// ports. It also handles full dill file read requests from the isolate
/// and redirects them to the asset server.
///
/// Uses [_address] and [_port] to communicate and [_assetHandler] to
/// redirect asset requests to the asset server.
///
/// [_sdkRoot] is the path to the directory containing the sdk summary files,
/// [_workerPath] is the path to the DDC worker snapshot,
/// [_librariesPath] is the path to libraries definitions file
/// libraries.json.
///
/// Users need to stop the service by calling [stop].
class ExpressionCompilerService implements ExpressionCompiler {
final _compiler = Completer<_Compiler>();
final String _address;
final FutureOr<int> _port;
final Handler _assetHandler;
final bool _verbose;
final String _sdkRoot;
final String _librariesPath;
final String _workerPath;
ExpressionCompilerService(
this._address, this._port, this._assetHandler, this._verbose,
{String sdkRoot, String librariesPath, String workerPath})
: _sdkRoot = sdkRoot,
_librariesPath = librariesPath,
_workerPath = workerPath;
@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 =>
(await _compiler.future).compileExpressionToJs(isolateId, libraryUri,
line, column, jsModules, jsFrameValues, moduleName, expression);
@override
Future<void> initialize({String moduleFormat, bool soundNullSafety}) async {
if (_compiler.isCompleted) return;
soundNullSafety ??= false;
final executable = Platform.resolvedExecutable;
final binDir = p.dirname(executable);
final sdkDir = p.dirname(binDir);
var sdkRoot = _sdkRoot ?? p.join(sdkDir, 'lib', '_internal');
var sdkSummaryPath = soundNullSafety
? p.join(sdkRoot, 'ddc_outline_sound.dill')
: p.join(sdkRoot, 'ddc_sdk.dill');
var librariesPath =
_librariesPath ?? p.join(sdkDir, 'lib', 'libraries.json');
var workerPath =
_workerPath ?? p.join(binDir, 'snapshots', 'dartdevc.dart.snapshot');
var compiler = await _Compiler.start(_address, await _port, workerPath,
sdkSummaryPath, librariesPath, moduleFormat, _verbose, soundNullSafety);
_compiler.complete(compiler);
}
@override
Future<bool> updateDependencies(Map<String, ModuleInfo> modules) async =>
(await _compiler.future).updateDependencies(modules);
Future<void> stop() async {
if (_compiler.isCompleted) return (await _compiler.future).stop();
}
/// Handles resource requests from expression compiler worker.
///
/// Handles REST get requests of the form:
/// http://host:port/getResource?uri=<resource uri>
///
/// Where the resource uri can be a package Uri for a dart file
/// or a server path for a full dill file.
/// Translates given resource uri to a server path and redirects
/// the request to the asset handler.
FutureOr<Response> handler(Request request) async {
var uri = request.requestedUri.queryParameters['uri'];
try {
var query = request.requestedUri.path;
_logger.finest('request: ${request.method} ${request.requestedUri}');
if (query != '/getResource' || uri == null) {
return Response.notFound(uri);
}
if (!uri.endsWith('.dart') && !uri.endsWith('.dill')) {
return Response.notFound(uri);
}
var serverPath = uri;
if (uri.endsWith('.dart')) {
serverPath = DartUri(uri).serverPath;
}
_logger.finest('serverpath for $uri: $serverPath');
request = Request(
request.method,
Uri(
scheme: request.requestedUri.scheme,
host: request.requestedUri.host,
port: request.requestedUri.port,
path: serverPath,
),
protocolVersion: request.protocolVersion,
context: request.context,
headers: request.headers,
handlerPath: request.handlerPath,
encoding: request.encoding,
);
return await _assetHandler(request);
} catch (e, s) {
_logger.severe('Error loading $uri: $e:$s');
rethrow;
}
}
}