| // 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 = new Logger('InstrumentationInputConverter'); |
| final Set<String> eventsSeen = new Set<String>(); |
| |
| /** |
| * A mapping from request/response id to request json |
| * for those requests for which a response has not been processed. |
| */ |
| final Map<String, dynamic> 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, dynamic> 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, dynamic> asMap(dynamic value) => value as Map<String, dynamic>; |
| |
| /** |
| * Return an operation for the notification or `null` if none. |
| */ |
| Operation convertNotification(Map<String, dynamic> json) { |
| String event = json['event']; |
| if (event == SERVER_NOTIFICATION_STATUS) { |
| // {"event":"server.status","params":{"analysis":{"isAnalyzing":false}}} |
| Map<String, dynamic> params = asMap(json['params']); |
| if (params != null) { |
| Map<String, dynamic> analysis = asMap(params['analysis']); |
| if (analysis != null && analysis['isAnalyzing'] == false) { |
| return new WaitForAnalysisCompleteOperation(); |
| } |
| } |
| } |
| if (event == SERVER_NOTIFICATION_CONNECTED) { |
| // {"event":"server.connected","params":{"version":"1.7.0"}} |
| return new 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, dynamic> origJson) { |
| Map<String, dynamic> json = asMap(translateSrcPaths(origJson)); |
| requestMap[json['id']] = json; |
| String method = json['method']; |
| // 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 |
| Request request = new Request.fromJson(json); |
| var params = new AnalysisUpdateContentParams.fromRequest(request); |
| params.files.forEach((String filePath, change) { |
| if (change is AddContentOverlay) { |
| String content = change.content; |
| if (content == null) { |
| throw 'expected new overlay content\n$json'; |
| } |
| overlays[filePath] = content; |
| } else if (change is ChangeContentOverlay) { |
| String 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) { |
| String 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'; |
| } |
| new File(filePath).writeAsStringSync(content); |
| } else { |
| throw 'unknown overlay change $change\n$json'; |
| } |
| }); |
| return new RequestOperation(this, json); |
| } |
| // Track performance for completion notifications |
| if (method == COMPLETION_REQUEST_GET_SUGGESTIONS) { |
| return new 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 new RequestOperation(this, json); |
| } |
| throw 'unknown request: $method\n $json'; |
| } |
| |
| /** |
| * Return an operation for the recorded/expected response. |
| */ |
| Operation convertResponse(Map<String, dynamic> json) { |
| return new ResponseOperation(this, asMap(requestMap.remove(json['id'])), |
| asMap(translateSrcPaths(json))); |
| } |
| |
| void logOverlayContent() { |
| logger.log(Level.WARNING, '${overlays.length} overlays'); |
| List<String> allPaths = overlays.keys.toList()..sort(); |
| for (String 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) { |
| if (exception.message.startsWith(ERROR_PREFIX)) { |
| result = json.decode(exception.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 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) { |
| Completer 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. |
| */ |
| translateSrcPaths(json) { |
| if (json is String) { |
| return srcPathMap.translate(json); |
| } |
| if (json is List) { |
| List result = []; |
| for (int i = 0; i < json.length; ++i) { |
| result.add(translateSrcPaths(json[i])); |
| } |
| return result; |
| } |
| if (json is Map) { |
| Map<String, dynamic> result = new Map<String, dynamic>(); |
| json.forEach((origKey, value) { |
| result[translateSrcPaths(origKey)] = 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 = new 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; |
| } |
| if (converter != null) { |
| try { |
| return converter.convert(line); |
| } catch (e) { |
| active = false; |
| rethrow; |
| } |
| } |
| if (headerLineCount == 20) { |
| throw 'Failed to determine input file format'; |
| } |
| if (InstrumentationInputConverter.isFormat(line)) { |
| converter = new InstrumentationInputConverter(tmpSrcDirPath, srcPathMap); |
| } else if (LogFileInputConverter.isFormat(line)) { |
| converter = new LogFileInputConverter(tmpSrcDirPath, srcPathMap); |
| } |
| if (converter != null) { |
| return converter.convert(line); |
| } |
| logger.log(Level.INFO, 'skipped input line: $line'); |
| return null; |
| } |
| |
| @override |
| _InputSink startChunkedConversion(outSink) { |
| return new _InputSink(this, outSink); |
| } |
| } |
| |
| /** |
| * 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(new PathMapEntry(oldSrcPrefix, newSrcPrefix)); |
| } |
| |
| String translate(String original) { |
| String result = original; |
| for (PathMapEntry 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 outSink; |
| |
| _InputSink(this.converter, this.outSink); |
| |
| @override |
| void add(String line) { |
| Operation op = converter.convert(line); |
| if (op != null) { |
| outSink.add(op); |
| } |
| } |
| |
| @override |
| void close() { |
| outSink.close(); |
| } |
| } |