blob: b2b5139909688413b97afa82cb241faed5b97cac [file] [log] [blame] [edit]
// Copyright (c) 2013, 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.
part of "dart:_vmservice";
final class _CompileExpressionErrorDetails {
final String details;
_CompileExpressionErrorDetails(this.details);
}
/// The message in an error response from the resident frontend compiler can
/// either be in the 'errorMessage' property or the 'compilerOutputLines'
/// property of the response.
String _extractErrorMessageFromResidentFrontendCompilerResponse(
Map<String, dynamic> response,
) {
const errorMessageString = 'errorMessage';
if (response[errorMessageString] != null) {
return response[errorMessageString];
} else {
return (response['compilerOutputLines'] as List<dynamic>)
.cast<String>()
.join('\n');
}
}
class RunningIsolates implements MessageRouter {
static const _isolateIdString = 'isolateId';
static const _successString = 'success';
static const _useCachedCompilerOptionsAsBaseString =
'useCachedCompilerOptionsAsBase';
final isolates = <int, RunningIsolate>{};
int? _rootPortId;
RunningIsolates();
void isolateStartup(int portId, SendPort sp, String name) {
if (_rootPortId == null) {
_rootPortId = portId;
}
var ri = RunningIsolate(portId, sp, name);
isolates[portId] = ri;
}
void isolateShutdown(int portId, SendPort sp) {
if (_rootPortId == portId) {
_rootPortId = null;
}
(isolates.remove(portId))?.onIsolateExit();
}
Future<Response> _handleReloadSourcesRequest(
VMService service,
Message message,
RunningIsolate isolate,
) async {
if (VMServiceEmbedderHooks.getResidentCompilerInfoFile!() == null) {
// If there isn't a resident frontend compiler available, we let the VM
// take care of the request.
return isolate.routeRequest(service, message);
} else {
const rootLibUriString = 'rootLibUri';
final String rootLibUri;
if (message.params[rootLibUriString] == null) {
// If a 'rootLibUri' property was not included in the request, we have
// to ask the VM for [isolate]'s root library URI.
final getIsolateRequest = Message.forMethod('getIsolate');
getIsolateRequest.params[_isolateIdString] =
message.params[isolate.serviceId];
final getIsolateResponse = await isolate.routeRequest(
service,
getIsolateRequest,
);
final isolateJson =
(getIsolateResponse.decodeJson() as Map<String, dynamic>)['result']
as Map<String, dynamic>;
final rootLibJson = isolateJson['rootLib'] as Map<String, dynamic>;
rootLibUri = rootLibJson['uri'];
} else {
rootLibUri = message.params[rootLibUriString];
}
final tempDirectory = Directory.systemTemp.createTempSync();
final outputDill = File(
'${tempDirectory.path}${Platform.pathSeparator}for_hot_reload.dill',
);
final responseFromResidentCompiler =
await _sendRequestToResidentFrontendCompilerAndRecieveResponse(
jsonEncode(<String, Object?>{
'command': 'compile',
_useCachedCompilerOptionsAsBaseString: true,
'executable': Uri.parse(rootLibUri).toFilePath(),
'output-dill': outputDill.path,
}),
VMServiceEmbedderHooks.getResidentCompilerInfoFile!()!,
);
if (responseFromResidentCompiler[_successString] == false) {
return Response.from(
encodeRpcError(
message,
kInternalError,
details: _extractErrorMessageFromResidentFrontendCompilerResponse(
responseFromResidentCompiler,
),
),
);
}
final reloadKernelRequest = Message.forMethod('_reloadKernel');
reloadKernelRequest.params[_isolateIdString] =
message.params[isolate.serviceId];
reloadKernelRequest.params['kernelFilePath'] = outputDill.uri
.toFilePath();
final response = await isolate.routeRequest(service, message);
tempDirectory.deleteSync(recursive: true);
return response;
}
}
@override
Future<Response> routeRequest(VMService service, Message message) {
String isolateParam = message.params['isolateId']! as String;
int isolateId;
if (!isolateParam.startsWith('isolates/')) {
message.setErrorResponse(
kInvalidParams,
"invalid 'isolateId' parameter: $isolateParam",
);
return message.response;
}
isolateParam = isolateParam.substring('isolates/'.length);
if (isolateParam == 'root') {
isolateId = _rootPortId!;
} else {
try {
isolateId = int.parse(isolateParam);
} catch (e) {
message.setErrorResponse(
kInvalidParams,
"invalid 'isolateId' parameter: $isolateParam",
);
return message.response;
}
}
final isolate = isolates[isolateId];
if (isolate == null) {
// There is some chance that this isolate may have lived before,
// so return a sentinel rather than an error.
final result = <String, String>{
'type': 'Sentinel',
'kind': 'Collected',
'valueAsString': '<collected>',
};
message.setResponse(encodeResult(message, result));
return message.response;
}
if (message.method == 'evaluateInFrame' || message.method == 'evaluate') {
return _Evaluator(message, isolate, service).run();
} else if (message.method == 'reloadSources') {
return _handleReloadSourcesRequest(service, message, isolate);
} else {
return isolate.routeRequest(service, message);
}
}
@override
void routeResponse(Message message) {}
}
// NOTE: The following class is a duplicate of one in
// 'package:frontend_server/resident_frontend_server_utils.dart'. We are forced
// to duplicate it because `dart:_vmservice` is not allowed to import
// `package:frontend_server`.
final class _ResidentCompilerInfo {
/// The SDK hash that kernel files compiled using the Resident Frontend
/// Compiler associated with this object will be stamped with.
final String? sdkHash;
/// The address that the Resident Frontend Compiler associated with this
/// object is listening from.
final InternetAddress address;
/// The port number that the Resident Frontend Compiler associated with this
/// object is listening on.
final int port;
/// Extracts the value associated with a key from [entries], where [entries]
/// is a [String] with the format '$key1:$value1 $key2:$value2 $key3:$value3 ...'.
static String _extractValueAssociatedWithKey(String entries, String key) =>
RegExp(
'$key:'
r'(\S+)(\s|$)',
).allMatches(entries).first[1]!;
static _ResidentCompilerInfo fromFile(File file) {
final fileContents = file.readAsStringSync();
return _ResidentCompilerInfo._(
sdkHash: fileContents.contains('sdkHash:')
? _extractValueAssociatedWithKey(fileContents, 'sdkHash')
: null,
address: InternetAddress(
_extractValueAssociatedWithKey(fileContents, 'address'),
),
port: int.parse(_extractValueAssociatedWithKey(fileContents, 'port')),
);
}
_ResidentCompilerInfo._({
required this.sdkHash,
required this.port,
required this.address,
});
}
// NOTE: The following function is a duplicate of one in
// 'package:frontend_server/resident_frontend_server_utils.dart'. We are
// forced to duplicate it because `dart:_vmservice` is not allowed to import
// `package:frontend_server`.
/// Sends a compilation [request] to the resident frontend compiler associated
/// with [serverInfoFile], and returns the compiler's JSON response.
///
/// Throws a [FileSystemException] if [serverInfoFile] cannot be accessed.
Future<Map<String, dynamic>>
_sendRequestToResidentFrontendCompilerAndRecieveResponse(
String request,
File serverInfoFile,
) async {
Socket? client;
Map<String, dynamic> jsonResponse;
final residentCompilerInfo = _ResidentCompilerInfo.fromFile(serverInfoFile);
try {
client = await Socket.connect(
residentCompilerInfo.address,
residentCompilerInfo.port,
);
client.write(request);
final data = String.fromCharCodes(await client.first);
jsonResponse = jsonDecode(data);
} catch (e) {
jsonResponse = <String, dynamic>{
'success': false,
'errorMessage': e.toString(),
};
}
client?.destroy();
return jsonResponse;
}
/// Class that knows how to orchestrate expression evaluation in dart2 world.
class _Evaluator {
static const _kernelBytesString = 'kernelBytes';
static const _compileExpressionString = 'compileExpression';
static const _expressionString = 'expression';
static const _definitionsString = 'definitions';
static const _definitionTypesString = 'definitionTypes';
static const _typeDefinitionsString = 'typeDefinitions';
static const _typeBoundsString = 'typeBounds';
static const _typeDefaultsString = 'typeDefaults';
static const _libraryUriString = 'libraryUri';
static const _tokenPosString = 'tokenPos';
static const _isStaticString = 'isStatic';
static const _scriptUriString = 'scriptUri';
static const _methodString = 'method';
static const _rootLibraryUriString = 'rootLibraryUri';
_Evaluator(this._message, this._isolate, this._service);
Future<Response> run() async {
final buildScopeResponse = await _buildScope();
final responseJson =
buildScopeResponse.decodeJson() as Map<String, dynamic>;
if (responseJson.containsKey('error')) {
final error = responseJson['error'] as Map<String, dynamic>;
final data = error['data'] as Map<String, dynamic>;
return Response.from(
encodeCompilationError(_message, data['details'] as String),
);
}
String kernelBase64;
try {
kernelBase64 = await _compileExpression(
responseJson['result'] as Map<String, dynamic>,
);
} on _CompileExpressionErrorDetails catch (e) {
return Response.from(
encodeRpcError(
_message,
kExpressionCompilationError,
details: e.details,
),
);
}
return await _evaluateCompiledExpression(kernelBase64);
}
Message _message;
RunningIsolate _isolate;
VMService _service;
Future<Response> _buildScope() {
final params = _setupParams();
params['isolateId'] = _message.params['isolateId'];
final buildScopeParams = <String, dynamic>{
'method': '_buildExpressionEvaluationScope',
'id': _message.serial,
'params': params,
};
if (_message.params['scope'] != null) {
(buildScopeParams['params'] as Map<String, dynamic>)['scope'] =
_message.params['scope'];
}
final buildScope = Message._fromJsonRpcRequest(
_message.client!,
buildScopeParams,
);
// Decode the JSON and insert it into the map. The map key
// is the request Uri.
return _isolate.routeRequest(_service, buildScope);
}
/// If [response] represents a valid JSON-RPC result, then this function
/// returns the 'kernelBytes' property of that result. Otherwise, this
/// function throws a [_CompileExpressionErrorDetails] object wrapping the
/// 'details' property of the JSON-RPC error.
static String _getKernelBytesOrThrowErrorDetails(
Map<String, dynamic> response,
) {
if (response['result'] != null) {
return (response['result'] as Map<String, dynamic>)[_kernelBytesString]
as String;
}
final error = response['error'] as Map<String, dynamic>;
final data = error['data'] as Map<String, dynamic>;
throw _CompileExpressionErrorDetails(data['details']);
}
/// If compilation fails, this method will throw a
/// [_CompileExpressionErrorDetails] object that will be used to populate the
/// 'details' field of the response to the evaluation RPC that requested this
/// compilation to happen.
Future<String> _compileExpression(
Map<String, dynamic> buildScopeResponseResult,
) async {
Client? externalClient = _service._findFirstClientThatHandlesService(
_compileExpressionString,
);
final compileParams = <String, dynamic>{
RunningIsolates._isolateIdString:
_message.params[RunningIsolates._isolateIdString]!,
_expressionString: _message.params[_expressionString]!,
_definitionsString: buildScopeResponseResult['param_names']!,
_definitionTypesString: buildScopeResponseResult['param_types']!,
_typeDefinitionsString: buildScopeResponseResult['type_params_names']!,
_typeBoundsString: buildScopeResponseResult['type_params_bounds']!,
_typeDefaultsString: buildScopeResponseResult['type_params_defaults']!,
_libraryUriString: buildScopeResponseResult[_libraryUriString]!,
_tokenPosString: buildScopeResponseResult[_tokenPosString]!,
_isStaticString: buildScopeResponseResult[_isStaticString]!,
};
final klass = buildScopeResponseResult['klass'];
if (klass != null) {
compileParams['klass'] = klass;
}
final scriptUri = buildScopeResponseResult[_scriptUriString];
if (scriptUri != null) {
compileParams[_scriptUriString] = scriptUri;
}
final method = buildScopeResponseResult[_methodString];
if (method != null) {
compileParams[_methodString] = method;
}
if (externalClient != null) {
// Let the external client handle expression compilation.
final compileExpression = Message.forMethod(_compileExpressionString);
compileExpression.client = externalClient;
compileExpression.params.addAll(compileParams);
final id = _service._serviceRequests.newId();
final oldId = _message.serial;
final completer = Completer<String>();
externalClient.serviceHandles[id] = (Message? m) {
if (m != null) {
completer.complete(json.encode(m.forwardToJson({'id': oldId})));
} else {
completer.complete(encodeRpcError(_message, kServiceDisappeared));
}
};
externalClient.post(
Response.json(
compileExpression.forwardToJson({
'id': id,
_methodString: _compileExpressionString,
}),
),
);
return completer.future
.then((s) => jsonDecode(s))
.then(
(json) => _getKernelBytesOrThrowErrorDetails(
json as Map<String, dynamic>,
),
);
} else if (VMServiceEmbedderHooks.getResidentCompilerInfoFile!() != null) {
// Compile the expression using the resident compiler.
final response =
await _sendRequestToResidentFrontendCompilerAndRecieveResponse(
jsonEncode({
'command': _compileExpressionString,
RunningIsolates._useCachedCompilerOptionsAsBaseString: true,
_expressionString: compileParams[_expressionString],
_definitionsString: compileParams[_definitionsString],
_definitionTypesString: compileParams[_definitionTypesString],
_typeDefinitionsString: compileParams[_typeDefinitionsString],
_typeBoundsString: compileParams[_typeBoundsString],
_typeDefaultsString: compileParams[_typeDefaultsString],
_libraryUriString: compileParams[_libraryUriString],
'offset': compileParams[_tokenPosString],
_isStaticString: compileParams[_isStaticString],
'class': compileParams['klass'],
_scriptUriString: compileParams[_scriptUriString],
_methodString: compileParams[_methodString],
_rootLibraryUriString:
buildScopeResponseResult[_rootLibraryUriString],
}),
VMServiceEmbedderHooks.getResidentCompilerInfoFile!()!,
);
if (response[RunningIsolates._successString] == true) {
return response[_kernelBytesString];
} else {
throw _CompileExpressionErrorDetails(
_extractErrorMessageFromResidentFrontendCompilerResponse(response),
);
}
} else {
// fallback to compile using kernel service
final compileExpressionParams = <String, dynamic>{
'method': '_compileExpression',
'id': _message.serial,
'params': compileParams,
};
final compileExpression = Message._fromJsonRpcRequest(
_message.client!,
compileExpressionParams,
);
return _isolate
.routeRequest(_service, compileExpression)
.then((response) => response.decodeJson())
.then(
(json) => _getKernelBytesOrThrowErrorDetails(
json as Map<String, dynamic>,
),
);
}
}
Future<Response> _evaluateCompiledExpression(String kernelBase64) {
if (kernelBase64.isNotEmpty) {
final params = _setupParams();
params['isolateId'] = _message.params['isolateId'];
params['kernelBytes'] = kernelBase64;
params['disableBreakpoints'] = _message.params['disableBreakpoints'];
final runParams = <String, dynamic>{
'method': '_evaluateCompiledExpression',
'id': _message.serial,
'params': params,
};
if (_message.params['scope'] != null) {
(runParams['params'] as Map<String, dynamic>)['scope'] =
_message.params['scope'];
}
if (_message.params['idZoneId'] != null) {
(runParams['params'] as Map<String, dynamic>)['idZoneId'] =
_message.params['idZoneId'];
}
final runExpression = Message._fromJsonRpcRequest(
_message.client!,
runParams,
);
return _isolate.routeRequest(_service, runExpression); // _message
} else {
// empty kernel indicates dart1 mode
return _isolate.routeRequest(_service, _message);
}
}
Map<String, dynamic> _setupParams() {
if (_message.method == 'evaluateInFrame') {
return <String, dynamic>{'frameIndex': _message.params['frameIndex']};
} else {
assert(_message.method == 'evaluate');
return <String, dynamic>{'targetId': _message.params['targetId']};
}
}
}