// 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. /// Support for interacting with an analysis server that is running in a /// separate process. import 'dart:async'; import 'dart:collection'; import 'dart:convert' hide JsonDecoder; import 'dart:io'; import 'dart:math' as math; import 'package:analysis_server/protocol/protocol.dart'; import 'package:analysis_server/protocol/protocol_generated.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:path/path.dart' as path; import 'logger.dart'; /// Return the current time expressed as milliseconds since the epoch. int get currentTime => DateTime.now().millisecondsSinceEpoch; /// ??? class ErrorMap { /// A table mapping file paths to the errors associated with that file. final Map> pathMap = HashMap>(); /// Initialize a newly created error map. ErrorMap(); /// Initialize a newly created error map to contain the same mapping as the /// given [errorMap]. ErrorMap.from(ErrorMap errorMap) { pathMap.addAll(errorMap.pathMap); } void operator []=(String filePath, List errors) { pathMap[filePath] = errors; } } /// Data that has been collected about a request sent to the server. class RequestData { /// The unique id of the request. final String id; /// The method that was requested. final String method; /// The request parameters, or `null` if there are no parameters. final Map? params; /// The time at which the request was sent. final int requestTime; /// The time at which the response was received, or `null` if no response has /// been received. int? responseTime; /// The response that was received. Response? _response; /// The completer that will be completed when a response is received. Completer? _responseCompleter; /// Initialize a newly created set of request data. RequestData(this.id, this.method, this.params, this.requestTime); /// Return the number of milliseconds that elapsed between the request and the /// response. This getter assumes that the response was received. int get elapsedTime => responseTime! - requestTime; /// Return a future that will complete when a response is received. Future get respondedTo { if (_response != null) { return Future.value(_response); } var completer = _responseCompleter ??= Completer(); return completer.future; } /// Record that the given [response] was received. void recordResponse(Response response) { if (_response != null) { stdout.writeln( 'Received a second response to a $method request (id = $id)'); return; } responseTime = currentTime; _response = response; var completer = _responseCompleter; if (completer != null) { completer.complete(response); _responseCompleter = null; } } } /// A utility for starting and communicating with an analysis server that is /// running in a separate process. class Server { /// The label used for communications from the client. static const String fromClient = 'client'; /// The label used for normal communications from the server. static const String fromServer = 'server'; /// The label used for output written by the server on [fromStderr]. static const String fromStderr = 'stderr'; /// The logger to which the communications log should be written, or `null` if /// the log should not be written. final Logger? logger; /// The process in which the server is running, or `null` if the server hasn't /// been started yet. Process? _process; /// Number that should be used to compute the 'id' to send in the next command /// sent to the server. int _nextId = 0; /// The analysis roots that are included. List _analysisRootIncludes = []; /// A list containing the paths of files for which an overlay has been /// created. List filesWithOverlays = []; /// The files that the server reported as being analyzed. List _analyzedFiles = []; /// A mapping from the absolute paths of files to the most recent set of /// errors received for that file. final ErrorMap _errorMap = ErrorMap(); /// The completer that will be completed the next time a 'server.status' /// notification is received from the server with 'analyzing' set to false. Completer? _analysisFinishedCompleter; /// The completer that will be completed the next time a 'server.connected' /// notification is received from the server. Completer? _serverConnectedCompleter; /// A table mapping the ids of requests that have been sent to the server to /// data about those requests. final Map _requestDataMap = {}; /// A table mapping the number of times a request whose 'event' is equal to /// the key was sent to the server. final Map _notificationCountMap = {}; /// Initialize a new analysis server. The analysis server is not running and /// must be started using [start]. /// /// If a [logger] is provided, the communications between the client (this /// test) and the server will be written to it. Server({this.logger}); /// Return a future that will complete when a 'server.status' notification is /// received from the server with 'analyzing' set to false. /// /// The future will only be completed by 'server.status' notifications that /// are received after this function call, so it is safe to use this getter /// multiple times in one test; each time it is used it will wait afresh for /// analysis to finish. Future get analysisFinished { var completer = _analysisFinishedCompleter ??= Completer(); return completer.future; } /// Return a list of the paths of files that are currently being analyzed. List get analyzedDartFiles { bool isAnalyzed(String filePath) { // TODO(brianwilkerson) This should use the path package to determine // inclusion, and needs to take exclusions into account. for (var includedRoot in _analysisRootIncludes) { if (filePath.startsWith(includedRoot)) { return true; } } return false; } var analyzedFiles = []; for (var filePath in _analyzedFiles) { if (filePath.endsWith('.dart') && isAnalyzed(filePath)) { analyzedFiles.add(filePath); } } return analyzedFiles; } /// Return a table mapping the absolute paths of files to the most recent set /// of errors received for that file. The content of the map will not change /// when new sets of errors are received. ErrorMap get errorMap => ErrorMap.from(_errorMap); /// Compute a mapping from each of the file paths in the given list of /// [filePaths] to the list of errors in the file at that path. Future computeErrorMap(List filePaths) async { var errorMap = ErrorMap(); var futures = []; for (var filePath in filePaths) { var requestData = sendAnalysisGetErrors(filePath); futures.add(requestData.respondedTo.then((Response response) { if (response.result != null) { var result = AnalysisGetErrorsResult.fromResponse(response); errorMap[filePath] = result.errors; } })); } await Future.wait(futures); return errorMap; } /// Print information about the communications with the server. void printStatistics() { void writeSpaces(int count) { for (var i = 0; i < count; i++) { stdout.write(' '); } } // // Print information about the requests that were sent. // stdout.writeln('Request Counts'); if (_requestDataMap.isEmpty) { stdout.writeln(' none'); } else { var requestsByMethod = >{}; _requestDataMap.values.forEach((RequestData requestData) { requestsByMethod .putIfAbsent(requestData.method, () => []) .add(requestData); }); var keys = requestsByMethod.keys.toList(); keys.sort(); var maxCount = requestsByMethod.values .fold(0, (int count, List list) => count + list.length); var countWidth = maxCount.toString().length; for (var key in keys) { var requests = requestsByMethod[key]!; var noResponseCount = 0; var responseCount = 0; var minTime = -1; var maxTime = -1; var totalTime = 0; requests.forEach((RequestData data) { if (data.responseTime == null) { noResponseCount++; } else { responseCount++; var time = data.elapsedTime; minTime = minTime < 0 ? time : math.min(minTime, time); maxTime = math.max(maxTime, time); totalTime += time; } }); var count = requests.length.toString(); writeSpaces(countWidth - count.length); stdout.write(' '); stdout.write(count); stdout.write(' - '); stdout.write(key); if (noResponseCount > 0) { stdout.write(', '); stdout.write(noResponseCount); stdout.write(' with no response'); } if (maxTime >= 0) { stdout.write(' ('); stdout.write(minTime); stdout.write(', '); stdout.write(totalTime / responseCount); stdout.write(', '); stdout.write(maxTime); stdout.write(')'); } stdout.writeln(); } } // // Print information about the notifications that were received. // stdout.writeln(); stdout.writeln('Notification Counts'); if (_notificationCountMap.isEmpty) { stdout.writeln(' none'); } else { var keys = _notificationCountMap.keys.toList(); keys.sort(); var maxCount = _notificationCountMap.values.fold(0, math.max); var countWidth = maxCount.toString().length; for (var key in keys) { var count = _notificationCountMap[key].toString(); writeSpaces(countWidth - count.length); stdout.write(' '); stdout.write(count); stdout.write(' - '); stdout.writeln(key); } } } /// Remove any existing overlays. void removeAllOverlays() { var files = {}; for (var path in filesWithOverlays) { files[path] = RemoveContentOverlay(); } sendAnalysisUpdateContent(files); } RequestData sendAnalysisGetErrors(String file) { var params = AnalysisGetErrorsParams(file).toJson(); return _send('analysis.getErrors', params); } RequestData sendAnalysisGetHover(String file, int offset) { var params = AnalysisGetHoverParams(file, offset).toJson(); return _send('analysis.getHover', params); } RequestData sendAnalysisGetLibraryDependencies() { return _send('analysis.getLibraryDependencies', null); } RequestData sendAnalysisGetNavigation(String file, int offset, int length) { var params = AnalysisGetNavigationParams(file, offset, length).toJson(); return _send('analysis.getNavigation', params); } RequestData sendAnalysisGetReachableSources(String file) { var params = AnalysisGetReachableSourcesParams(file).toJson(); return _send('analysis.getReachableSources', params); } void sendAnalysisReanalyze() { var params = AnalysisReanalyzeParams().toJson(); _send('analysis.reanalyze', params); } void sendAnalysisSetAnalysisRoots( List included, List excluded, {Map? packageRoots}) { _analysisRootIncludes = included; var params = AnalysisSetAnalysisRootsParams(included, excluded, packageRoots: packageRoots) .toJson(); _send('analysis.setAnalysisRoots', params); } void sendAnalysisSetGeneralSubscriptions( List subscriptions) { var params = AnalysisSetGeneralSubscriptionsParams(subscriptions).toJson(); _send('analysis.setGeneralSubscriptions', params); } void sendAnalysisSetPriorityFiles(List files) { var params = AnalysisSetPriorityFilesParams(files).toJson(); _send('analysis.setPriorityFiles', params); } void sendAnalysisSetSubscriptions( Map> subscriptions) { var params = AnalysisSetSubscriptionsParams(subscriptions).toJson(); _send('analysis.setSubscriptions', params); } void sendAnalysisUpdateContent(Map files) { files.forEach((path, overlay) { if (overlay is AddContentOverlay) { filesWithOverlays.add(path); } else if (overlay is RemoveContentOverlay) { filesWithOverlays.remove(path); } }); var params = AnalysisUpdateContentParams(files).toJson(); _send('analysis.updateContent', params); } void sendAnalysisUpdateOptions(AnalysisOptions options) { var params = AnalysisUpdateOptionsParams(options).toJson(); _send('analysis.updateOptions', params); } void sendCompletionGetSuggestions(String file, int offset) { var params = CompletionGetSuggestionsParams(file, offset).toJson(); _send('completion.getSuggestions', params); } RequestData sendDiagnosticGetDiagnostics() { return _send('diagnostic.getDiagnostics', null); } RequestData sendEditFormat( String file, int selectionOffset, int selectionLength, {int? lineLength}) { var params = EditFormatParams(file, selectionOffset, selectionLength, lineLength: lineLength) .toJson(); return _send('edit.format', params); } RequestData sendEditGetAssists(String file, int offset, int length) { var params = EditGetAssistsParams(file, offset, length).toJson(); return _send('edit.getAssists', params); } RequestData sendEditGetAvailableRefactorings( String file, int offset, int length) { var params = EditGetAvailableRefactoringsParams(file, offset, length).toJson(); return _send('edit.getAvailableRefactorings', params); } RequestData sendEditGetFixes(String file, int offset) { var params = EditGetFixesParams(file, offset).toJson(); return _send('edit.getFixes', params); } RequestData sendEditGetRefactoring(RefactoringKind kind, String file, int offset, int length, bool validateOnly, {RefactoringOptions? options}) { var params = EditGetRefactoringParams( kind, file, offset, length, validateOnly, options: options) .toJson(); return _send('edit.getRefactoring', params); } RequestData sendEditOrganizeDirectives(String file) { var params = EditOrganizeDirectivesParams(file).toJson(); return _send('edit.organizeDirectives', params); } RequestData sendEditSortMembers(String file) { var params = EditSortMembersParams(file).toJson(); return _send('edit.sortMembers', params); } RequestData sendExecutionCreateContext(String contextRoot) { var params = ExecutionCreateContextParams(contextRoot).toJson(); return _send('execution.createContext', params); } RequestData sendExecutionDeleteContext(String id) { var params = ExecutionDeleteContextParams(id).toJson(); return _send('execution.deleteContext', params); } RequestData sendExecutionMapUri(String id, {String? file, String? uri}) { var params = ExecutionMapUriParams(id, file: file, uri: uri).toJson(); return _send('execution.mapUri', params); } RequestData sendExecutionSetSubscriptions( List subscriptions) { var params = ExecutionSetSubscriptionsParams(subscriptions).toJson(); return _send('execution.setSubscriptions', params); } void sendSearchFindElementReferences( String file, int offset, bool includePotential) { var params = SearchFindElementReferencesParams(file, offset, includePotential) .toJson(); _send('search.findElementReferences', params); } void sendSearchFindMemberDeclarations(String name) { var params = SearchFindMemberDeclarationsParams(name).toJson(); _send('search.findMemberDeclarations', params); } void sendSearchFindMemberReferences(String name) { var params = SearchFindMemberReferencesParams(name).toJson(); _send('search.findMemberReferences', params); } void sendSearchFindTopLevelDeclarations(String pattern) { var params = SearchFindTopLevelDeclarationsParams(pattern).toJson(); _send('search.findTopLevelDeclarations', params); } void sendSearchGetTypeHierarchy(String file, int offset, {bool? superOnly}) { var params = SearchGetTypeHierarchyParams(file, offset, superOnly: superOnly) .toJson(); _send('search.getTypeHierarchy', params); } RequestData sendServerGetVersion() { return _send('server.getVersion', null); } void sendServerSetSubscriptions(List subscriptions) { var params = ServerSetSubscriptionsParams(subscriptions).toJson(); _send('server.setSubscriptions', params); } void sendServerShutdown() { _send('server.shutdown', null); } /// Start the server and listen for communications from it. /// /// If [checked] is `true`, the server's VM will be running in checked mode. /// /// If [diagnosticPort] is not `null`, the server will serve status pages to /// the specified port. /// /// If [profileServer] is `true`, the server will be started with "--observe" /// and "--pause-isolates-on-exit", allowing the observatory to be used. /// /// If [useAnalysisHighlight2] is `true`, the server will use the new /// highlight APIs. Future start( {bool checked = true, int? diagnosticPort, bool profileServer = false, String? sdkPath, int? servicesPort, bool useAnalysisHighlight2 = false}) async { if (_process != null) { throw Exception('Process already started'); } var dartBinary = Platform.executable; var rootDir = _findRoot(Platform.script.toFilePath(windows: Platform.isWindows)); var serverPath = path.normalize(path.join(rootDir, 'bin', 'server.dart')); var arguments = []; // // Add VM arguments. // if (profileServer) { if (servicesPort == null) { arguments.add('--observe'); } else { arguments.add('--observe=$servicesPort'); } arguments.add('--pause-isolates-on-exit'); } else if (servicesPort != null) { arguments.add('--enable-vm-service=$servicesPort'); } if (Platform.packageConfig != null) { arguments.add('--packages=${Platform.packageConfig}'); } if (checked) { arguments.add('--checked'); } // // Add the server executable. // arguments.add(serverPath); // // Add server arguments. // if (diagnosticPort != null) { arguments.add('--port'); arguments.add(diagnosticPort.toString()); } if (sdkPath != null) { arguments.add('--sdk=$sdkPath'); } if (useAnalysisHighlight2) { arguments.add('--useAnalysisHighlight2'); } // stdout.writeln('Launching $serverPath'); // stdout.writeln('$dartBinary ${arguments.join(' ')}'); _process = await Process.start(dartBinary, arguments); _process!.exitCode.then((int code) { if (code != 0) { throw StateError('Server terminated with exit code $code'); } }); _listenToOutput(); var completer = Completer(); _serverConnectedCompleter = completer; return completer.future; } /// Find the root directory of the analysis_server package by proceeding /// upward to the 'test' dir, and then going up one more directory. String _findRoot(String pathname) { while (!['benchmark', 'test'].contains(path.basename(pathname))) { var parent = path.dirname(pathname); if (parent.length >= pathname.length) { throw Exception("Can't find root directory"); } pathname = parent; } return path.dirname(pathname); } /// Handle a [notification] received from the server. void _handleNotification(Notification notification) { switch (notification.event) { case 'server.connected': // new ServerConnectedParams.fromNotification(notification); _serverConnectedCompleter!.complete(null); break; case 'server.error': // new ServerErrorParams.fromNotification(notification); throw StateError('Server error: ${notification.toJson()}'); case 'server.status': if (_analysisFinishedCompleter != null) { var params = ServerStatusParams.fromNotification(notification); var analysis = params.analysis; if (analysis != null && !analysis.isAnalyzing) { _analysisFinishedCompleter!.complete(null); } } break; case 'analysis.analyzedFiles': var params = AnalysisAnalyzedFilesParams.fromNotification(notification); _analyzedFiles = params.directories; break; case 'analysis.errors': var params = AnalysisErrorsParams.fromNotification(notification); _errorMap.pathMap[params.file] = params.errors; break; case 'analysis.flushResults': // new AnalysisFlushResultsParams.fromNotification(notification); _errorMap.pathMap.clear(); break; case 'analysis.folding': // new AnalysisFoldingParams.fromNotification(notification); break; case 'analysis.highlights': // new AnalysisHighlightsParams.fromNotification(notification); break; case 'analysis.implemented': // new AnalysisImplementedParams.fromNotification(notification); break; case 'analysis.invalidate': // new AnalysisInvalidateParams.fromNotification(notification); break; case 'analysis.navigation': // new AnalysisNavigationParams.fromNotification(notification); break; case 'analysis.occurrences': // new AnalysisOccurrencesParams.fromNotification(notification); break; case 'analysis.outline': // new AnalysisOutlineParams.fromNotification(notification); break; case 'analysis.overrides': // new AnalysisOverridesParams.fromNotification(notification); break; case 'completion.results': // new CompletionResultsParams.fromNotification(notification); break; case 'search.results': // new SearchResultsParams.fromNotification(notification); break; case 'execution.launchData': // new ExecutionLaunchDataParams.fromNotification(notification); break; default: throw StateError('Unhandled notification: ${notification.toJson()}'); } } /// Handle a [response] received from the server. void _handleResponse(Response response) { var id = response.id.toString(); var requestData = _requestDataMap[id]!; requestData.recordResponse(response); // switch (requestData.method) { // case "analysis.getErrors": // break; // case "analysis.getHover": // break; // case "analysis.getLibraryDependencies": // break; // case "analysis.getNavigation": // break; // case "analysis.getReachableSources": // break; // case "analysis.reanalyze": // break; // case "analysis.setAnalysisRoots": // break; // case "analysis.setGeneralSubscriptions": // break; // case "analysis.setPriorityFiles": // break; // case "analysis.setSubscriptions": // break; // case 'analysis.updateContent': // break; // case "analysis.updateOptions": // break; // case "completion.getSuggestions": // break; // case "diagnostic.getDiagnostics": // break; // case "edit.format": // break; // case "edit.getAssists": // break; // case "edit.getAvailableRefactorings": // break; // case "edit.getFixes": // break; // case "edit.getRefactoring": // break; // case "edit.organizeDirectives": // break; // case "edit.sortMembers": // break; // case "execution.createContext": // break; // case "execution.deleteContext": // break; // case "execution.mapUri": // break; // case "execution.setSubscriptions": // break; // case "search.findElementReferences": // break; // case "search.findMemberDeclarations": // break; // case "search.findMemberReferences": // break; // case "search.findTopLevelDeclarations": // break; // case "search.getTypeHierarchy": // break; // case "server.getVersion": // break; // case "server.setSubscriptions": // break; // case "server.shutdown": // break; // default: // throw new StateError('Unhandled response: ${response.toJson()}'); // } } /// Handle a [line] of input read from stderr. void _handleStdErr(String line) { var trimmedLine = line.trim(); logger?.log(fromStderr, '$trimmedLine'); throw StateError('Message received on stderr: "$trimmedLine"'); } /// Handle a [line] of input read from stdout. void _handleStdOut(String line) { /// Cast the given [value] to a Map, or throw an [ArgumentError] if the /// value cannot be cast. Map asMap(Object value) { if (value is Map) { return value; } throw ArgumentError('Expected a Map, found a ${value.runtimeType}'); } var trimmedLine = line.trim(); if (trimmedLine.isEmpty || trimmedLine.startsWith('Observatory listening on ')) { return; } logger?.log(fromServer, '$trimmedLine'); var message = asMap(json.decoder.convert(trimmedLine)); if (message.containsKey('id')) { // The message is a response. var response = Response.fromJson(message)!; _handleResponse(response); } else { // The message is a notification. var notification = Notification.fromJson(message); var event = notification.event; _notificationCountMap[event] = (_notificationCountMap[event] ?? 0) + 1; _handleNotification(notification); } } /// Start listening to output from the server. void _listenToOutput() { /// Install the given [handler] to listen to transformed output from the /// given [stream]. void installHandler( Stream> stream, void Function(String) handler) { stream .transform(Utf8Codec().decoder) .transform(LineSplitter()) .listen(handler); } installHandler(_process!.stdout, _handleStdOut); installHandler(_process!.stderr, _handleStdErr); } /// Send a command to the server. An 'id' will be automatically assigned. RequestData _send(String method, Map? params) { var id = '${_nextId++}'; var requestData = RequestData(id, method, params, currentTime); _requestDataMap[id] = requestData; var command = {'id': id, 'method': method}; if (params != null) { command['params'] = params; } var line = json.encode(command); _process!.stdin.add(utf8.encoder.convert('$line\n')); logger?.log(fromClient, '$line'); return requestData; } }