| // Copyright (c) 2014, 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. |
| |
| library get.handler; |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:math'; |
| |
| import 'package:analysis_server/src/analysis_server.dart'; |
| import 'package:analysis_server/src/domain_completion.dart'; |
| import 'package:analysis_server/src/socket_server.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/src/generated/engine.dart'; |
| import 'package:analyzer/src/generated/java_engine.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| |
| import 'analysis_server.dart'; |
| |
| /** |
| * Instances of the class [GetHandler] handle GET requests. |
| */ |
| class GetHandler { |
| /** |
| * The path used to request the status of the analysis server as a whole. |
| */ |
| static const String STATUS_PATH = '/status'; |
| |
| /** |
| * The path used to request information about the cache entry corresponding |
| * to a single file. |
| */ |
| static const String CACHE_ENTRY_PATH = '/cache_entry'; |
| |
| /** |
| * The path used to request the list of source files in a certain cache |
| * state. |
| */ |
| static const String CACHE_STATE_PATH = '/cache_state'; |
| |
| /** |
| * The path used to request code completion information. |
| */ |
| static const String COMPLETION_PATH = '/completion'; |
| |
| /** |
| * Query parameter used to represent the cache state to search for, when |
| * accessing [CACHE_STATE_PATH]. |
| */ |
| static const String STATE_QUERY_PARAM = 'state'; |
| |
| /** |
| * Query parameter used to represent the context to search for, when |
| * accessing [CACHE_ENTRY_PATH] or [CACHE_STATE_PATH]. |
| */ |
| static const String CONTEXT_QUERY_PARAM = 'context'; |
| |
| /** |
| * Query parameter used to represent the descriptor to search for, when |
| * accessing [CACHE_STATE_PATH]. |
| */ |
| static const String DESCRIPTOR_QUERY_PARAM = 'descriptor'; |
| |
| /** |
| * Query parameter used to represent the source to search for, when accessing |
| * [CACHE_ENTRY_PATH]. |
| */ |
| static const String SOURCE_QUERY_PARAM = 'entry'; |
| |
| /** |
| * The socket server whose status is to be reported on. |
| */ |
| SocketServer _server; |
| |
| /** |
| * Buffer containing strings printed by the analysis server. |
| */ |
| List<String> _printBuffer; |
| |
| /** |
| * Initialize a newly created handler for GET requests. |
| */ |
| GetHandler(this._server, this._printBuffer); |
| |
| /** |
| * Handle a GET request received by the HTTP server. |
| */ |
| void handleGetRequest(HttpRequest request) { |
| String path = request.uri.path; |
| if (path == STATUS_PATH) { |
| _returnServerStatus(request); |
| } else if (path == CACHE_STATE_PATH) { |
| _returnCacheState(request); |
| } else if (path == CACHE_ENTRY_PATH) { |
| _returnCacheEntry(request); |
| } else if (path == COMPLETION_PATH) { |
| _returnCompletionInfo(request); |
| } else { |
| _returnUnknownRequest(request); |
| } |
| } |
| |
| /** |
| * Create a link to [path] with query parameters [params], with inner HTML |
| * [innerHtml]. |
| */ |
| String _makeLink(String path, Map<String, String> params, String innerHtml) { |
| Uri uri = new Uri(path: path, queryParameters: params); |
| return '<a href="${HTML_ESCAPE.convert(uri.toString())}">$innerHtml</a>'; |
| } |
| |
| /** |
| * Generate a table showing the cache values corresponding to the given |
| * [descriptors], using [getState] to get the cache state corresponding to |
| * each descriptor, and [getValue] to get the cached value corresponding to |
| * each descriptor. Append the resulting HTML to [response]. |
| */ |
| void _outputDescriptorTable(HttpResponse response, |
| List<DataDescriptor> descriptors, CacheState getState(DataDescriptor), dynamic |
| getValue(DataDescriptor)) { |
| response.write('<dl>'); |
| for (DataDescriptor descriptor in descriptors) { |
| String descriptorName = HTML_ESCAPE.convert(descriptor.toString()); |
| String descriptorState = |
| HTML_ESCAPE.convert(getState(descriptor).toString()); |
| response.write('<dt>$descriptorName ($descriptorState)</dt><dd>'); |
| try { |
| _outputValueAsHtml(response, getValue(descriptor)); |
| } catch (exception) { |
| response.write('(${HTML_ESCAPE.convert(exception.toString())})'); |
| } |
| response.write('</dd>'); |
| } |
| response.write('</dl>'); |
| } |
| |
| /** |
| * Render the given [value] as HTML and append it to [response]. |
| */ |
| void _outputValueAsHtml(HttpResponse response, dynamic value) { |
| if (value == null) { |
| response.write('<i>null</i>'); |
| } else if (value is String) { |
| response.write('<pre>${HTML_ESCAPE.convert(value)}</pre>'); |
| } else if (value is List) { |
| response.write('${value.length} entries'); |
| response.write('<ul>'); |
| for (var entry in value) { |
| response.write('<li>'); |
| _outputValueAsHtml(response, entry); |
| response.write('</li>'); |
| } |
| response.write('</ul>'); |
| } else { |
| response.write(HTML_ESCAPE.convert(value.toString())); |
| response.write( |
| ' <i>(${HTML_ESCAPE.convert(value.runtimeType.toString())})</i>'); |
| } |
| } |
| |
| /** |
| * Return a response containing information about a single source file in the |
| * cache. |
| */ |
| void _returnCacheEntry(HttpRequest request) { |
| // Figure out which context is being searched for. |
| String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; |
| if (contextFilter == null) { |
| return _returnFailure( |
| request, |
| 'Query parameter $CONTEXT_QUERY_PARAM required'); |
| } |
| |
| // Figure out which CacheEntry is being searched for. |
| String sourceUri = request.uri.queryParameters[SOURCE_QUERY_PARAM]; |
| if (sourceUri == null) { |
| return _returnFailure( |
| request, |
| 'Query parameter $SOURCE_QUERY_PARAM required'); |
| } |
| |
| AnalysisServer analysisServer = _server.analysisServer; |
| if (analysisServer == null) { |
| return _returnFailure(request, 'Analysis server not running'); |
| } |
| HttpResponse response = request.response; |
| response.statusCode = HttpStatus.OK; |
| response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| response.write('<html>'); |
| response.write('<head>'); |
| response.write('<title>Dart Analysis Server - Search result</title>'); |
| response.write('</head>'); |
| response.write('<body>'); |
| response.write('<h1>'); |
| response.write('File ${HTML_ESCAPE.convert(sourceUri)}'); |
| response.write(' in context ${HTML_ESCAPE.convert(contextFilter)}'); |
| response.write('</h1>'); |
| analysisServer.folderMap.forEach( |
| (Folder folder, AnalysisContextImpl context) { |
| if (folder.path != contextFilter) { |
| return; |
| } |
| Source source = context.sourceFactory.forUri(sourceUri); |
| if (source == null) { |
| response.write('<p>Not found.</p>'); |
| return; |
| } |
| SourceEntry entry = context.getReadableSourceEntryOrNull(source); |
| if (entry == null) { |
| response.write('<p>Not found.</p>'); |
| return; |
| } |
| response.write('<h2>File info:</h2><dl>'); |
| _outputDescriptorTable( |
| response, |
| entry.descriptors, |
| entry.getState, |
| entry.getValue); |
| if (entry is DartEntry) { |
| for (Source librarySource in entry.containingLibraries) { |
| String libraryName = HTML_ESCAPE.convert(librarySource.fullName); |
| response.write('<h2>In library $libraryName:</h2>'); |
| _outputDescriptorTable( |
| response, |
| entry.libraryDescriptors, |
| (DataDescriptor descriptor) => |
| entry.getStateInLibrary(descriptor, librarySource), |
| (DataDescriptor descriptor) => |
| entry.getValueInLibrary(descriptor, librarySource)); |
| } |
| } |
| }); |
| response.write('</body>'); |
| response.write('</html>'); |
| response.close(); |
| } |
| |
| /** |
| * Return a response indicating the set of source files in a certain cache |
| * state. |
| */ |
| void _returnCacheState(HttpRequest request) { |
| // Figure out what CacheState is being searched for. |
| String stateQueryParam = request.uri.queryParameters[STATE_QUERY_PARAM]; |
| if (stateQueryParam == null) { |
| return _returnFailure( |
| request, |
| 'Query parameter $STATE_QUERY_PARAM required'); |
| } |
| CacheState stateFilter = null; |
| for (CacheState value in CacheState.values) { |
| if (value.toString() == stateQueryParam) { |
| stateFilter = value; |
| } |
| } |
| if (stateFilter == null) { |
| return _returnFailure( |
| request, |
| 'Query parameter $STATE_QUERY_PARAM is invalid'); |
| } |
| |
| // Figure out which context is being searched for. |
| String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; |
| if (contextFilter == null) { |
| return _returnFailure( |
| request, |
| 'Query parameter $CONTEXT_QUERY_PARAM required'); |
| } |
| |
| // Figure out which descriptor is being searched for. |
| String descriptorFilter = |
| request.uri.queryParameters[DESCRIPTOR_QUERY_PARAM]; |
| if (descriptorFilter == null) { |
| return _returnFailure( |
| request, |
| 'Query parameter $DESCRIPTOR_QUERY_PARAM required'); |
| } |
| |
| AnalysisServer analysisServer = _server.analysisServer; |
| if (analysisServer == null) { |
| return _returnFailure(request, 'Analysis server not running'); |
| } |
| HttpResponse response = request.response; |
| response.statusCode = HttpStatus.OK; |
| response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| response.write('<html>'); |
| response.write('<head>'); |
| response.write('<title>Dart Analysis Server - Search result</title>'); |
| response.write('</head>'); |
| response.write('<body>'); |
| response.write('<h1>'); |
| response.write('Files with state ${HTML_ESCAPE.convert(stateQueryParam)}'); |
| response.write(' for descriptor ${HTML_ESCAPE.convert(descriptorFilter)}'); |
| response.write(' in context ${HTML_ESCAPE.convert(contextFilter)}'); |
| response.write('</h1>'); |
| response.write('<ul>'); |
| int count = 0; |
| analysisServer.folderMap.forEach( |
| (Folder folder, AnalysisContextImpl context) { |
| if (folder.path != contextFilter) { |
| return; |
| } |
| context.visitCacheItems( |
| (Source source, SourceEntry dartEntry, DataDescriptor rowDesc, CacheState state) |
| { |
| if (state != stateFilter || rowDesc.toString() != descriptorFilter) { |
| return; |
| } |
| String link = _makeLink(CACHE_ENTRY_PATH, { |
| CONTEXT_QUERY_PARAM: folder.path, |
| SOURCE_QUERY_PARAM: source.uri.toString() |
| }, HTML_ESCAPE.convert(source.fullName)); |
| response.write('<li>$link</li>'); |
| count++; |
| }); |
| }); |
| response.write('</ul>'); |
| response.write('<p>$count files found</p>'); |
| response.write('</body>'); |
| response.write('</html>'); |
| response.close(); |
| } |
| |
| /** |
| * Return a response displaying code completion information. |
| */ |
| void _returnCompletionInfo(HttpRequest request) { |
| var refresh = request.requestedUri.queryParameters['refresh']; |
| var maxCount = request.requestedUri.queryParameters['maxCount']; |
| HttpResponse response = request.response; |
| response.statusCode = HttpStatus.OK; |
| response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| response.write('<html>'); |
| response.write('<head>'); |
| response.write('<title>Dart Analysis Server - Completion Stats</title>'); |
| response.write('<style>'); |
| response.write('td.right {text-align: right;}'); |
| response.write('</style>'); |
| if (refresh is String) { |
| int seconds = int.parse(refresh, onError: (_) => 5); |
| response.write('<meta http-equiv="refresh" content="$seconds">'); |
| } |
| response.write('</head>'); |
| response.write('<body>'); |
| _writeCompletionInfo(response, maxCount); |
| response.write('<p> </p>'); |
| response.write('<p>Try '); |
| response.write('<a href="?refresh=5">?refresh=5</a>'); |
| response.write(' to refresh every 5 seconds</p>'); |
| response.write('<p>and '); |
| response.write('<a href="?maxCount=50">?maxCount=50</a>'); |
| response.write(' to keep the last 50 performance measurements</p>'); |
| response.write('</body>'); |
| response.write('</html>'); |
| response.close(); |
| } |
| |
| void _returnFailure(HttpRequest request, String message) { |
| HttpResponse response = request.response; |
| response.statusCode = HttpStatus.OK; |
| response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| response.write('<html>'); |
| response.write('<head>'); |
| response.write('<title>Dart Analysis Server - Failure</title>'); |
| response.write('</head>'); |
| response.write('<body>'); |
| response.write(HTML_ESCAPE.convert(message)); |
| response.write('</body>'); |
| response.write('</html>'); |
| response.close(); |
| } |
| |
| /** |
| * Return a response indicating the status of the analysis server. |
| */ |
| void _returnServerStatus(HttpRequest request) { |
| HttpResponse response = request.response; |
| response.statusCode = HttpStatus.OK; |
| response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| response.write('<html>'); |
| response.write('<head>'); |
| response.write('<title>Dart Analysis Server - Status</title>'); |
| response.write('<style>'); |
| response.write('td.right {text-align: right;}'); |
| response.write('</style>'); |
| response.write('</head>'); |
| response.write('<body>'); |
| response.write('<h1>Analysis Server</h1>'); |
| AnalysisServer analysisServer = _server.analysisServer; |
| if (analysisServer == null) { |
| response.write('<p>Not running</p>'); |
| } else { |
| if (analysisServer.statusAnalyzing) { |
| response.write('<p>Running (analyzing)</p>'); |
| } else { |
| response.write('<p>Running (not analyzing)</p>'); |
| } |
| response.write('<h1>Analysis Contexts</h1>'); |
| response.write('<h2>Summary</h2>'); |
| response.write('<table>'); |
| List headerRowText = ['Context']; |
| headerRowText.addAll(CacheState.values); |
| _writeRow(response, headerRowText, header: true); |
| Map<Folder, AnalysisContext> folderMap = analysisServer.folderMap; |
| List<Folder> folders = folderMap.keys.toList(); |
| folders.sort( |
| (Folder first, Folder second) => first.shortName.compareTo(second.shortName)); |
| folders.forEach((Folder folder) { |
| AnalysisContextImpl context = folderMap[folder]; |
| String key = folder.shortName; |
| AnalysisContextStatistics statistics = context.statistics; |
| Map<CacheState, int> totals = <CacheState, int>{}; |
| for (CacheState state in CacheState.values) { |
| totals[state] = 0; |
| } |
| statistics.cacheRows.forEach((AnalysisContextStatistics_CacheRow row) { |
| for (CacheState state in CacheState.values) { |
| totals[state] += row.getCount(state); |
| } |
| }); |
| List rowText = [ |
| '<a href="#context_${HTML_ESCAPE.convert(key)}">$key</a>']; |
| for (CacheState state in CacheState.values) { |
| rowText.add(totals[state]); |
| } |
| _writeRow(response, rowText, classes: [null, "right"]); |
| }); |
| response.write('</table>'); |
| folders.forEach((Folder folder) { |
| AnalysisContextImpl context = folderMap[folder]; |
| String key = folder.shortName; |
| response.write( |
| '<h2><a name="context_${HTML_ESCAPE.convert(key)}">Analysis Context: $key</a></h2>'); |
| AnalysisContextStatistics statistics = context.statistics; |
| response.write('<table>'); |
| _writeRow(response, headerRowText, header: true); |
| statistics.cacheRows.forEach((AnalysisContextStatistics_CacheRow row) { |
| List rowText = [row.name]; |
| for (CacheState state in CacheState.values) { |
| String text = row.getCount(state).toString(); |
| Map<String, String> params = <String, String>{ |
| STATE_QUERY_PARAM: state.toString(), |
| CONTEXT_QUERY_PARAM: folder.path, |
| DESCRIPTOR_QUERY_PARAM: row.name |
| }; |
| rowText.add(_makeLink(CACHE_STATE_PATH, params, text)); |
| } |
| _writeRow(response, rowText, classes: [null, "right"]); |
| }); |
| response.write('</table>'); |
| List<CaughtException> exceptions = statistics.exceptions; |
| if (!exceptions.isEmpty) { |
| response.write('<h2>Exceptions</h2>'); |
| exceptions.forEach((CaughtException exception) { |
| response.write('<p>${exception.exception}</p>'); |
| }); |
| } |
| }); |
| } |
| response.write('<h1>Most recent strings printed by analysis server</h2>'); |
| response.write('<pre>'); |
| response.write(HTML_ESCAPE.convert(_printBuffer.join('\n'))); |
| response.write('</pre>'); |
| response.write('</body>'); |
| response.write('</html>'); |
| response.close(); |
| } |
| |
| /** |
| * Return an error in response to an unrecognized request received by the HTTP |
| * server. |
| */ |
| void _returnUnknownRequest(HttpRequest request) { |
| HttpResponse response = request.response; |
| response.statusCode = HttpStatus.NOT_FOUND; |
| response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| response.write('<html>'); |
| response.write('<head>'); |
| response.write('<title>Dart Analysis Server - Page Not Found</title>'); |
| response.write('</head>'); |
| response.write('<body>'); |
| response.write('<h1>Page Not Found</h1>'); |
| response.write('<p>Try one of these links instead:</p>'); |
| response.write('<ul>'); |
| response.write('<li><a href="$STATUS_PATH">Server Status</a></li>'); |
| response.write('<li><a href="$COMPLETION_PATH">Completion Stats</a></li>'); |
| response.write('<ul>'); |
| response.write('</body>'); |
| response.write('</html>'); |
| response.close(); |
| } |
| |
| /** |
| * Append code completion information. |
| */ |
| void _writeCompletionInfo(HttpResponse response, maxCount) { |
| response.write('<h1>Code Completion</h1>'); |
| AnalysisServer analysisServer = _server.analysisServer; |
| if (analysisServer == null) { |
| response.write('<p>Not running</p>'); |
| return; |
| } |
| CompletionDomainHandler handler = analysisServer.handlers.firstWhere( |
| (h) => h is CompletionDomainHandler, |
| orElse: () => null); |
| if (handler == null) { |
| response.write('<p>No code completion</p>'); |
| return; |
| } |
| if (maxCount is String) { |
| int count = int.parse(maxCount, onError: (_) => 0); |
| handler.performanceListMaxLength = count; |
| } |
| CompletionPerformance performance = handler.performance; |
| if (performance == null) { |
| response.write('<p>No performance stats yet</p>'); |
| return; |
| } |
| response.write('<table>'); |
| _writeRow(response, ['Elapsed', '', 'Operation'], header: true); |
| performance.operations.forEach((OperationPerformance op) { |
| String elapsed = op.elapsed != null ? op.elapsed.toString() : '???'; |
| _writeRow(response, [elapsed, ' ', op.name]); |
| }); |
| if (handler.priorityChangedPerformance == null) { |
| response.write('<p>No priorityChanged caching</p>'); |
| } else { |
| int len = handler.priorityChangedPerformance.operations.length; |
| if (len > 0) { |
| var op = handler.priorityChangedPerformance.operations[len - 1]; |
| if (op != null) { |
| _writeRow(response, [' ', ' ', ' ']); |
| String elapsed = op.elapsed != null ? op.elapsed.toString() : '???'; |
| _writeRow(response, [elapsed, ' ', op.name]); |
| } |
| } |
| } |
| response.write('</table>'); |
| if (handler.performanceList.length > 0) { |
| response.write('<table>'); |
| _writeRow(response, ['Milliseconds', '', '# Notifications', '', '# Suggestions', '', 'Snippet'], header: true); |
| handler.performanceList.forEach((CompletionPerformance performance) { |
| _writeRow( |
| response, |
| [ |
| performance.elapsedInMilliseconds, |
| ' ', |
| performance.notificationCount, |
| ' ', |
| performance.suggestionCount, |
| ' ', |
| performance.snippet]); |
| }); |
| response.write('</table>'); |
| } |
| } |
| |
| /** |
| * Write a single row within a table to the given [response] object. The row |
| * will have one cell for each of the [columns], and will be a header row if |
| * [header] is `true`. |
| */ |
| void _writeRow(HttpResponse response, List<Object> columns, {bool header: |
| false, List<String> classes}) { |
| response.write('<tr>'); |
| int count = columns.length; |
| int maxClassIndex = classes == null ? 0 : classes.length - 1; |
| for (int i = 0; i < count; i++) { |
| String classAttribute = ''; |
| if (classes != null) { |
| String className = classes[min(i, maxClassIndex)]; |
| if (className != null) { |
| classAttribute = ' class="$className"'; |
| } |
| } |
| if (header) { |
| response.write('<th$classAttribute>'); |
| } else { |
| response.write('<td$classAttribute>'); |
| } |
| response.write(columns[i]); |
| if (header) { |
| response.write('</th>'); |
| } else { |
| response.write('</td>'); |
| } |
| } |
| |
| response.write('</tr>'); |
| } |
| } |