| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library input.transformer; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:convert'; | |
| 9 import 'dart:io'; | |
| 10 | |
| 11 import 'package:analysis_server/src/constants.dart'; | |
| 12 import 'package:analysis_server/src/protocol.dart'; | |
| 13 import 'package:logging/logging.dart'; | |
| 14 import 'package:path/path.dart' as path; | |
| 15 | |
| 16 import 'instrumentation_input_converter.dart'; | |
| 17 import 'log_file_input_converter.dart'; | |
| 18 import 'operation.dart'; | |
| 19 | |
| 20 /** | |
| 21 * Common input converter superclass for sharing implementation. | |
| 22 */ | |
| 23 abstract class CommonInputConverter extends Converter<String, Operation> { | |
| 24 static final ERROR_PREFIX = 'Server responded with an error: '; | |
| 25 final Logger logger = new Logger('InstrumentationInputConverter'); | |
| 26 final Set<String> eventsSeen = new Set<String>(); | |
| 27 | |
| 28 /** | |
| 29 * A mapping from request/response id to request json | |
| 30 * for those requests for which a response has not been processed. | |
| 31 */ | |
| 32 final Map<String, dynamic> requestMap = {}; | |
| 33 | |
| 34 /** | |
| 35 * A mapping from request/response id to a completer | |
| 36 * for those requests for which a response has not been processed. | |
| 37 * The completer is called with the actual json response | |
| 38 * when it becomes available. | |
| 39 */ | |
| 40 final Map<String, Completer> responseCompleters = {}; | |
| 41 | |
| 42 /** | |
| 43 * A mapping from request/response id to the actual response result | |
| 44 * for those responses that have not been processed. | |
| 45 */ | |
| 46 final Map<String, dynamic> responseMap = {}; | |
| 47 | |
| 48 /** | |
| 49 * A mapping of current overlay content | |
| 50 * parallel to what is in the analysis server | |
| 51 * so that we can update the file system. | |
| 52 */ | |
| 53 final Map<String, String> overlays = {}; | |
| 54 | |
| 55 /** | |
| 56 * The prefix used to determine if a request parameter is a file path. | |
| 57 */ | |
| 58 final String rootPrefix = path.rootPrefix(path.current); | |
| 59 | |
| 60 /** | |
| 61 * A mapping of source path prefixes | |
| 62 * from location where instrumentation or log file was generated | |
| 63 * to the target location of the source using during performance measurement. | |
| 64 */ | |
| 65 final Map<String, String> srcPathMap; | |
| 66 | |
| 67 /** | |
| 68 * The root directory for all source being modified | |
| 69 * during performance measurement. | |
| 70 */ | |
| 71 final String tmpSrcDirPath; | |
| 72 | |
| 73 /** | |
| 74 * The diagnostic port for Analysis Server or `null` if none. | |
| 75 */ | |
| 76 final int diagnosticPort; | |
| 77 | |
| 78 CommonInputConverter(this.tmpSrcDirPath, this.srcPathMap, | |
| 79 {this.diagnosticPort}); | |
| 80 | |
| 81 /** | |
| 82 * Return an operation for the notification or `null` if none. | |
| 83 */ | |
| 84 Operation convertNotification(Map<String, dynamic> json) { | |
| 85 String event = json['event']; | |
| 86 if (event == SERVER_STATUS) { | |
| 87 // {"event":"server.status","params":{"analysis":{"isAnalyzing":false}}} | |
| 88 Map<String, dynamic> params = json['params']; | |
| 89 if (params != null) { | |
| 90 Map<String, dynamic> analysis = params['analysis']; | |
| 91 if (analysis != null && analysis['isAnalyzing'] == false) { | |
| 92 return new WaitForAnalysisCompleteOperation(); | |
| 93 } | |
| 94 } | |
| 95 } | |
| 96 if (event == SERVER_CONNECTED) { | |
| 97 // {"event":"server.connected","params":{"version":"1.7.0"}} | |
| 98 return new StartServerOperation(diagnosticPort: diagnosticPort); | |
| 99 } | |
| 100 if (eventsSeen.add(event)) { | |
| 101 logger.log(Level.INFO, 'Ignored notification: $event\n $json'); | |
| 102 } | |
| 103 return null; | |
| 104 } | |
| 105 | |
| 106 /** | |
| 107 * Return an operation for the request or `null` if none. | |
| 108 */ | |
| 109 Operation convertRequest(Map<String, dynamic> origJson) { | |
| 110 Map<String, dynamic> json = translateSrcPaths(origJson); | |
| 111 requestMap[json['id']] = json; | |
| 112 String method = json['method']; | |
| 113 // Sanity check operations that modify source | |
| 114 // to ensure that the operation is on source in temp space | |
| 115 if (method == ANALYSIS_UPDATE_CONTENT) { | |
| 116 // Track overlays in parallel with the analysis server | |
| 117 // so that when an overlay is removed, the file can be updated on disk | |
| 118 Request request = new Request.fromJson(json); | |
| 119 var params = new AnalysisUpdateContentParams.fromRequest(request); | |
| 120 params.files.forEach((String filePath, change) { | |
| 121 if (change is AddContentOverlay) { | |
| 122 String content = change.content; | |
| 123 if (content == null) { | |
| 124 throw 'expected new overlay content\n$json'; | |
| 125 } | |
| 126 overlays[filePath] = content; | |
| 127 } else if (change is ChangeContentOverlay) { | |
| 128 String content = overlays[filePath]; | |
| 129 if (content == null) { | |
| 130 throw 'expected cached overlay content\n$json'; | |
| 131 } | |
| 132 overlays[filePath] = SourceEdit.applySequence(content, change.edits); | |
| 133 } else if (change is RemoveContentOverlay) { | |
| 134 String content = overlays.remove(filePath); | |
| 135 if (content == null) { | |
| 136 throw 'expected cached overlay content\n$json'; | |
| 137 } | |
| 138 if (!path.isWithin(tmpSrcDirPath, filePath)) { | |
| 139 throw 'found path referencing source outside temp space\n$filePath\n
$json'; | |
| 140 } | |
| 141 new File(filePath).writeAsStringSync(content); | |
| 142 } else { | |
| 143 throw 'unknown overlay change $change\n$json'; | |
| 144 } | |
| 145 }); | |
| 146 return new RequestOperation(this, json); | |
| 147 } | |
| 148 // Track performance for completion notifications | |
| 149 if (method == COMPLETION_GET_SUGGESTIONS) { | |
| 150 return new CompletionRequestOperation(this, json); | |
| 151 } | |
| 152 // TODO(danrubel) replace this with code | |
| 153 // that just forwards the translated request | |
| 154 if (method == ANALYSIS_GET_HOVER || | |
| 155 method == ANALYSIS_SET_ANALYSIS_ROOTS || | |
| 156 method == ANALYSIS_SET_PRIORITY_FILES || | |
| 157 method == ANALYSIS_SET_SUBSCRIPTIONS || | |
| 158 method == ANALYSIS_UPDATE_OPTIONS || | |
| 159 method == EDIT_GET_ASSISTS || | |
| 160 method == EDIT_GET_AVAILABLE_REFACTORINGS || | |
| 161 method == EDIT_GET_FIXES || | |
| 162 method == EDIT_GET_REFACTORING || | |
| 163 method == EDIT_SORT_MEMBERS || | |
| 164 method == EXECUTION_CREATE_CONTEXT || | |
| 165 method == EXECUTION_DELETE_CONTEXT || | |
| 166 method == EXECUTION_MAP_URI || | |
| 167 method == EXECUTION_SET_SUBSCRIPTIONS || | |
| 168 method == SEARCH_FIND_ELEMENT_REFERENCES || | |
| 169 method == SEARCH_FIND_MEMBER_DECLARATIONS || | |
| 170 method == SERVER_GET_VERSION || | |
| 171 method == SERVER_SET_SUBSCRIPTIONS) { | |
| 172 return new RequestOperation(this, json); | |
| 173 } | |
| 174 throw 'unknown request: $method\n $json'; | |
| 175 } | |
| 176 | |
| 177 /** | |
| 178 * Return an operation for the recorded/expected response. | |
| 179 */ | |
| 180 Operation convertResponse(Map<String, dynamic> json) { | |
| 181 return new ResponseOperation( | |
| 182 this, requestMap.remove(json['id']), translateSrcPaths(json)); | |
| 183 } | |
| 184 | |
| 185 /** | |
| 186 * Process an error response from the server by either | |
| 187 * completing the associated completer in the [responseCompleters] | |
| 188 * or stashing it in [responseMap] if no completer exists. | |
| 189 */ | |
| 190 void processErrorResponse(String id, exception) { | |
| 191 var result = exception; | |
| 192 if (exception is UnimplementedError) { | |
| 193 if (exception.message.startsWith(ERROR_PREFIX)) { | |
| 194 result = JSON.decode(exception.message.substring(ERROR_PREFIX.length)); | |
| 195 } | |
| 196 } | |
| 197 processResponseResult(id, result); | |
| 198 } | |
| 199 | |
| 200 /** | |
| 201 * Process the expected response by completing the given completer | |
| 202 * with the result if it has alredy been received, | |
| 203 * or caching the completer to be completed when the server | |
| 204 * returns the associated result. | |
| 205 * Return a future that completes when the response is received | |
| 206 * or `null` if the response has already been received | |
| 207 * and the completer completed. | |
| 208 */ | |
| 209 Future processExpectedResponse(String id, Completer completer) { | |
| 210 if (responseMap.containsKey(id)) { | |
| 211 logger.log(Level.INFO, 'processing cached response $id'); | |
| 212 completer.complete(responseMap.remove(id)); | |
| 213 return null; | |
| 214 } else { | |
| 215 logger.log(Level.INFO, 'waiting for response $id'); | |
| 216 responseCompleters[id] = completer; | |
| 217 return completer.future; | |
| 218 } | |
| 219 } | |
| 220 | |
| 221 /** | |
| 222 * Process a success response result from the server by either | |
| 223 * completing the associated completer in the [responseCompleters] | |
| 224 * or stashing it in [responseMap] if no completer exists. | |
| 225 * The response result may be `null`. | |
| 226 */ | |
| 227 void processResponseResult(String id, result) { | |
| 228 Completer completer = responseCompleters[id]; | |
| 229 if (completer != null) { | |
| 230 logger.log(Level.INFO, 'processing response $id'); | |
| 231 completer.complete(result); | |
| 232 } else { | |
| 233 logger.log(Level.INFO, 'caching response $id'); | |
| 234 responseMap[id] = result; | |
| 235 } | |
| 236 } | |
| 237 | |
| 238 /** | |
| 239 * Recursively translate source paths in the specified JSON to reference | |
| 240 * the temporary source used during performance measurement rather than | |
| 241 * the original source when the instrumentation or log file was generated. | |
| 242 */ | |
| 243 translateSrcPaths(json) { | |
| 244 if (json is String) { | |
| 245 String result = json; | |
| 246 srcPathMap.forEach((String oldPrefix, String newPrefix) { | |
| 247 if (json.startsWith(oldPrefix)) { | |
| 248 result = '$newPrefix${json.substring(oldPrefix.length)}'; | |
| 249 } | |
| 250 }); | |
| 251 return result; | |
| 252 } | |
| 253 if (json is List) { | |
| 254 List result = []; | |
| 255 for (int i = 0; i < json.length; ++i) { | |
| 256 result.add(translateSrcPaths(json[i])); | |
| 257 } | |
| 258 return result; | |
| 259 } | |
| 260 if (json is Map) { | |
| 261 Map<String, dynamic> result = new Map<String, dynamic>(); | |
| 262 json.forEach((String origKey, value) { | |
| 263 result[translateSrcPaths(origKey)] = translateSrcPaths(value); | |
| 264 }); | |
| 265 return result; | |
| 266 } | |
| 267 return json; | |
| 268 } | |
| 269 } | |
| 270 | |
| 271 /** | |
| 272 * [InputConverter] converts an input stream | |
| 273 * into a series of operations to be sent to the analysis server. | |
| 274 * The input stream can be either an instrumenation or log file. | |
| 275 */ | |
| 276 class InputConverter extends Converter<String, Operation> { | |
| 277 final Logger logger = new Logger('InputConverter'); | |
| 278 | |
| 279 /** | |
| 280 * A mapping of source path prefixes | |
| 281 * from location where instrumentation or log file was generated | |
| 282 * to the target location of the source using during performance measurement. | |
| 283 */ | |
| 284 final Map<String, String> srcPathMap; | |
| 285 | |
| 286 /** | |
| 287 * The root directory for all source being modified | |
| 288 * during performance measurement. | |
| 289 */ | |
| 290 final String tmpSrcDirPath; | |
| 291 | |
| 292 /** | |
| 293 * The diagnostic port for Analysis Server or `null` if none. | |
| 294 */ | |
| 295 final int diagnosticPort; | |
| 296 | |
| 297 /** | |
| 298 * The number of lines read before the underlying converter was determined | |
| 299 * or the end of file was reached. | |
| 300 */ | |
| 301 int headerLineCount = 0; | |
| 302 | |
| 303 /** | |
| 304 * The underlying converter used to translate lines into operations | |
| 305 * or `null` if it has not yet been determined. | |
| 306 */ | |
| 307 Converter<String, Operation> converter; | |
| 308 | |
| 309 /** | |
| 310 * [active] is `true` if converting lines to operations | |
| 311 * or `false` if an exception has occurred. | |
| 312 */ | |
| 313 bool active = true; | |
| 314 | |
| 315 InputConverter(this.tmpSrcDirPath, this.srcPathMap, {this.diagnosticPort}); | |
| 316 | |
| 317 @override | |
| 318 Operation convert(String line) { | |
| 319 if (!active) { | |
| 320 return null; | |
| 321 } | |
| 322 if (converter != null) { | |
| 323 try { | |
| 324 return converter.convert(line); | |
| 325 } catch (e) { | |
| 326 active = false; | |
| 327 rethrow; | |
| 328 } | |
| 329 } | |
| 330 if (headerLineCount == 20) { | |
| 331 throw 'Failed to determine input file format'; | |
| 332 } | |
| 333 if (InstrumentationInputConverter.isFormat(line)) { | |
| 334 converter = new InstrumentationInputConverter(tmpSrcDirPath, srcPathMap, | |
| 335 diagnosticPort: diagnosticPort); | |
| 336 } else if (LogFileInputConverter.isFormat(line)) { | |
| 337 converter = new LogFileInputConverter(tmpSrcDirPath, srcPathMap, | |
| 338 diagnosticPort: diagnosticPort); | |
| 339 } | |
| 340 if (converter != null) { | |
| 341 return converter.convert(line); | |
| 342 } | |
| 343 logger.log(Level.INFO, 'skipped input line: $line'); | |
| 344 return null; | |
| 345 } | |
| 346 | |
| 347 @override | |
| 348 _InputSink startChunkedConversion(outSink) { | |
| 349 return new _InputSink(this, outSink); | |
| 350 } | |
| 351 } | |
| 352 | |
| 353 class _InputSink extends ChunkedConversionSink<String> { | |
| 354 final Converter<String, Operation> converter; | |
| 355 final outSink; | |
| 356 | |
| 357 _InputSink(this.converter, this.outSink); | |
| 358 | |
| 359 @override | |
| 360 void add(String line) { | |
| 361 Operation op = converter.convert(line); | |
| 362 if (op != null) { | |
| 363 outSink.add(op); | |
| 364 } | |
| 365 } | |
| 366 | |
| 367 @override | |
| 368 void close() { | |
| 369 outSink.close(); | |
| 370 } | |
| 371 } | |
| OLD | NEW |