blob: 0c8a9b5e36fef193fc02e96139b7b00bbe83c0c2 [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:isolate';
import 'package:async/async.dart';
import 'package:logging/logging.dart';
import 'package:shelf/shelf.dart';
import '../utilities/dart_uri.dart';
import '../utilities/sdk_configuration.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.
///
/// [sdkConfiguration] describes the locations of SDK files used in
/// expression compilation (summaries, libraries spec, compiler worker
/// snapshot).
///
/// [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 moduleFormat,
bool soundNullSafety,
SdkConfiguration sdkConfiguration,
bool verbose,
) async {
sdkConfiguration.validate();
final librariesUri = sdkConfiguration.librariesUri;
final workerUri = sdkConfiguration.compilerWorkerUri;
final sdkSummaryUri = soundNullSafety
? sdkConfiguration.soundSdkSummaryUri
: sdkConfiguration.unsoundSdkSummaryUri;
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,
);
final responseQueue = StreamQueue(receivePort);
final sendPort = await responseQueue.next as SendPort;
final 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');
}
final updateCompleter = Completer();
_dependencyUpdate = updateCompleter.future;
_logger.info('Updating dependencies...');
_logger.finest('Dependencies: $modules');
final 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
},
]
});
final result = response == null ? false : response['succeeded'] as bool;
if (result) {
_logger.info('Updated dependencies.');
} else {
final e = response['exception'];
final 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');
final 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) {
final errors = response['errors'] as List<String>;
final e = response['exception'];
final s = response['stackTrace'];
final error = (errors != null && errors.isNotEmpty)
? errors.first
: (e != null ? '$e:$s' : '<unknown error>');
final 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.
///
/// Configuration created by [_sdkConfigurationProvider] describes the
/// locations of SDK files used in expression compilation (summaries,
/// libraries spec, compiler worker snapshot).
///
/// 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 SdkConfigurationProvider _sdkConfigurationProvider;
ExpressionCompilerService(
this._address,
this._port,
this._assetHandler, {
bool verbose = false,
SdkConfigurationProvider sdkConfigurationProvider,
}) : _verbose = verbose,
_sdkConfigurationProvider =
sdkConfigurationProvider ?? DefaultSdkConfigurationProvider();
@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 = false}) async {
if (_compiler.isCompleted) return;
final compiler = await _Compiler.start(
_address,
await _port,
moduleFormat,
soundNullSafety,
await _sdkConfigurationProvider.configuration,
_verbose,
);
_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 {
final uri = request.requestedUri.queryParameters['uri'];
try {
final 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;
}
}
}