blob: fd45405aa3847edc18a019c8f14cc3d649bd1d66 [file] [log] [blame]
// Copyright (c) 2015, 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.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'instrumentation_input_converter.dart';
import 'log_file_input_converter.dart';
import 'operation.dart';
/// Common input converter superclass for sharing implementation.
abstract class CommonInputConverter extends Converter<String, Operation?> {
static final ERROR_PREFIX = 'Server responded with an error: ';
final Logger logger = Logger('InstrumentationInputConverter');
final Set<String> eventsSeen = <String>{};
/// A mapping from request/response id to request json
/// for those requests for which a response has not been processed.
final Map<String, Object?> requestMap = {};
/// A mapping from request/response id to a completer
/// for those requests for which a response has not been processed.
/// The completer is called with the actual json response
/// when it becomes available.
final Map<String, Completer> responseCompleters = {};
/// A mapping from request/response id to the actual response result
/// for those responses that have not been processed.
final Map<String, Object?> responseMap = {};
/// A mapping of current overlay content
/// parallel to what is in the analysis server
/// so that we can update the file system.
final Map<String, String> overlays = {};
/// The prefix used to determine if a request parameter is a file path.
final String rootPrefix = path.rootPrefix(path.current);
/// A mapping of source path prefixes
/// from location where instrumentation or log file was generated
/// to the target location of the source using during performance measurement.
final PathMap srcPathMap;
/// The root directory for all source being modified
/// during performance measurement.
final String tmpSrcDirPath;
CommonInputConverter(this.tmpSrcDirPath, this.srcPathMap);
Map<String, Object?> asMap(dynamic value) => value as Map<String, Object?>;
Map<String, Object?>? asMap2(dynamic value) => value as Map<String, Object?>?;
/// Return an operation for the notification or `null` if none.
Operation? convertNotification(Map<String, dynamic> json) {
var event = json['event'] as String;
if (event == SERVER_NOTIFICATION_STATUS) {
// {"event":"server.status","params":{"analysis":{"isAnalyzing":false}}}
var params = asMap2(json['params']);
if (params != null) {
var analysis = asMap2(params['analysis']);
if (analysis != null && analysis['isAnalyzing'] == false) {
return WaitForAnalysisCompleteOperation();
}
}
}
if (event == SERVER_NOTIFICATION_CONNECTED) {
// {"event":"server.connected","params":{"version":"1.7.0"}}
return StartServerOperation();
}
if (eventsSeen.add(event)) {
logger.log(Level.INFO, 'Ignored notification: $event\n $json');
}
return null;
}
/// Return an operation for the request or `null` if none.
Operation convertRequest(Map<String, Object?> origJson) {
var json = asMap(translateSrcPaths(origJson));
requestMap[json['id'] as String] = json;
var method = json['method'] as String;
// Sanity check operations that modify source
// to ensure that the operation is on source in temp space
if (method == ANALYSIS_REQUEST_UPDATE_CONTENT) {
// Track overlays in parallel with the analysis server
// so that when an overlay is removed, the file can be updated on disk
var request = Request.fromJson(json)!;
var params = AnalysisUpdateContentParams.fromRequest(request);
params.files.forEach((String filePath, change) {
if (change is AddContentOverlay) {
var content = change.content;
overlays[filePath] = content;
} else if (change is ChangeContentOverlay) {
var content = overlays[filePath];
if (content == null) {
throw 'expected cached overlay content\n$json';
}
overlays[filePath] = SourceEdit.applySequence(content, change.edits);
} else if (change is RemoveContentOverlay) {
var content = overlays.remove(filePath);
if (content == null) {
throw 'expected cached overlay content\n$json';
}
if (!path.isWithin(tmpSrcDirPath, filePath)) {
throw 'found path referencing source outside temp space\n$filePath\n$json';
}
File(filePath).writeAsStringSync(content);
} else {
throw 'unknown overlay change $change\n$json';
}
});
return RequestOperation(this, json);
}
// Track performance for completion notifications
if (method == COMPLETION_REQUEST_GET_SUGGESTIONS) {
return CompletionRequestOperation(this, json);
}
// TODO(danrubel) replace this with code
// that just forwards the translated request
if (method == ANALYSIS_REQUEST_GET_HOVER ||
method == ANALYSIS_REQUEST_SET_ANALYSIS_ROOTS ||
method == ANALYSIS_REQUEST_SET_PRIORITY_FILES ||
method == ANALYSIS_REQUEST_SET_SUBSCRIPTIONS ||
method == ANALYSIS_REQUEST_UPDATE_OPTIONS ||
method == EDIT_REQUEST_GET_ASSISTS ||
method == EDIT_REQUEST_GET_AVAILABLE_REFACTORINGS ||
method == EDIT_REQUEST_GET_FIXES ||
method == EDIT_REQUEST_GET_REFACTORING ||
method == EDIT_REQUEST_SORT_MEMBERS ||
method == EXECUTION_REQUEST_CREATE_CONTEXT ||
method == EXECUTION_REQUEST_DELETE_CONTEXT ||
method == EXECUTION_REQUEST_MAP_URI ||
method == EXECUTION_REQUEST_SET_SUBSCRIPTIONS ||
method == SEARCH_REQUEST_FIND_ELEMENT_REFERENCES ||
method == SEARCH_REQUEST_FIND_MEMBER_DECLARATIONS ||
method == SERVER_REQUEST_GET_VERSION ||
method == SERVER_REQUEST_SET_SUBSCRIPTIONS) {
return RequestOperation(this, json);
}
throw 'unknown request: $method\n $json';
}
/// Return an operation for the recorded/expected response.
Operation convertResponse(Map<String, dynamic> json) {
return ResponseOperation(this, asMap(requestMap.remove(json['id'])),
asMap(translateSrcPaths(json)));
}
void logOverlayContent() {
logger.log(Level.WARNING, '${overlays.length} overlays');
var allPaths = overlays.keys.toList()..sort();
for (var filePath in allPaths) {
logger.log(Level.WARNING, 'overlay $filePath\n${overlays[filePath]}');
}
}
/// Process an error response from the server by either
/// completing the associated completer in the [responseCompleters]
/// or stashing it in [responseMap] if no completer exists.
void processErrorResponse(String id, exception) {
var result = exception;
if (exception is UnimplementedError) {
var message = exception.message;
if (message!.startsWith(ERROR_PREFIX)) {
result = json.decode(message.substring(ERROR_PREFIX.length));
}
}
processResponseResult(id, result);
}
/// Process the expected response by completing the given completer
/// with the result if it has already been received,
/// or caching the completer to be completed when the server
/// returns the associated result.
/// Return a future that completes when the response is received
/// or `null` if the response has already been received
/// and the completer completed.
Future<void>? processExpectedResponse(String id, Completer completer) {
if (responseMap.containsKey(id)) {
logger.log(Level.INFO, 'processing cached response $id');
completer.complete(responseMap.remove(id));
return null;
} else {
logger.log(Level.INFO, 'waiting for response $id');
responseCompleters[id] = completer;
return completer.future;
}
}
/// Process a success response result from the server by either
/// completing the associated completer in the [responseCompleters]
/// or stashing it in [responseMap] if no completer exists.
/// The response result may be `null`.
void processResponseResult(String id, result) {
var completer = responseCompleters[id];
if (completer != null) {
logger.log(Level.INFO, 'processing response $id');
completer.complete(result);
} else {
logger.log(Level.INFO, 'caching response $id');
responseMap[id] = result;
}
}
/// Recursively translate source paths in the specified JSON to reference
/// the temporary source used during performance measurement rather than
/// the original source when the instrumentation or log file was generated.
Object? translateSrcPaths(Object? json) {
if (json is String) {
return srcPathMap.translate(json);
}
if (json is List) {
var result = <Object?>[];
for (var i = 0; i < json.length; ++i) {
result.add(translateSrcPaths(json[i] as Object?));
}
return result;
}
if (json is Map) {
var result = <String, Object?>{};
json.forEach((origKey, value) {
result[translateSrcPaths(origKey) as String] = translateSrcPaths(value);
});
return result;
}
return json;
}
}
/// [InputConverter] converts an input stream
/// into a series of operations to be sent to the analysis server.
/// The input stream can be either an instrumentation or log file.
class InputConverter extends Converter<String, Operation?> {
final Logger logger = Logger('InputConverter');
/// A mapping of source path prefixes
/// from location where instrumentation or log file was generated
/// to the target location of the source using during performance measurement.
final PathMap srcPathMap;
/// The root directory for all source being modified
/// during performance measurement.
final String tmpSrcDirPath;
/// The number of lines read before the underlying converter was determined
/// or the end of file was reached.
int _headerLineCount = 0;
/// The underlying converter used to translate lines into operations
/// or `null` if it has not yet been determined.
Converter<String, Operation?>? _converter;
/// [_active] is `true` if converting lines to operations
/// or `false` if an exception has occurred.
bool _active = true;
InputConverter(this.tmpSrcDirPath, this.srcPathMap);
@override
Operation? convert(String line) {
if (!_active) {
return null;
}
try {
var converter = _getConverter(line);
if (converter == null) {
logger.log(Level.INFO, 'skipped input line: $line');
return null;
}
return converter.convert(line);
} catch (_) {
_active = false;
rethrow;
}
}
@override
Sink<String> startChunkedConversion(outSink) {
return _InputSink(this, outSink);
}
/// Return the previously determined converter, or determine it from the
/// given [line]. Return `null` if cannot be determined yet. Throw an
/// exception if could not be determined after some number of tries.
Converter<String, Operation?>? _getConverter(String line) {
var converter = _converter;
if (converter != null) {
return converter;
}
if (_headerLineCount++ == 20) {
throw 'Failed to determine input file format';
}
if (InstrumentationInputConverter.isFormat(line)) {
_converter = InstrumentationInputConverter(tmpSrcDirPath, srcPathMap);
} else if (LogFileInputConverter.isFormat(line)) {
_converter = LogFileInputConverter(tmpSrcDirPath, srcPathMap);
}
return _converter;
}
}
/// A container of [PathMapEntry]s used to translate a source path in the log
/// before it is sent to the analysis server.
class PathMap {
final List<PathMapEntry> entries = [];
void add(String oldSrcPrefix, String newSrcPrefix) {
entries.add(PathMapEntry(oldSrcPrefix, newSrcPrefix));
}
String translate(String original) {
var result = original;
for (var entry in entries) {
result = entry.translate(result);
}
return result;
}
}
/// An entry in [PathMap] used to translate a source path in the log
/// before it is sent to the analysis server.
class PathMapEntry {
final String oldSrcPrefix;
final String newSrcPrefix;
PathMapEntry(this.oldSrcPrefix, this.newSrcPrefix);
String translate(String original) {
return original.startsWith(oldSrcPrefix)
? '$newSrcPrefix${original.substring(oldSrcPrefix.length)}'
: original;
}
}
class _InputSink extends ChunkedConversionSink<String> {
final Converter<String, Operation?> converter;
final Sink<Operation?> outSink;
_InputSink(this.converter, this.outSink);
@override
void add(String line) {
var op = converter.convert(line);
if (op != null) {
outSink.add(op);
}
}
@override
void close() {
outSink.close();
}
}