Chromium Code Reviews| Index: pkg/analysis_server/lib/src/get_handler.dart |
| diff --git a/pkg/analysis_server/lib/src/get_handler.dart b/pkg/analysis_server/lib/src/get_handler.dart |
| index 6be9b81977f3f43346e409c6667aa57c0c1909d4..457ec242c97dd66f367f00c259cb7af3d38b1fb8 100644 |
| --- a/pkg/analysis_server/lib/src/get_handler.dart |
| +++ b/pkg/analysis_server/lib/src/get_handler.dart |
| @@ -2,17 +2,26 @@ |
| // 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; |
| +library analysis_server.src.get_handler; |
| +import 'dart:collection'; |
| 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/domain_execution.dart'; |
| +import 'package:analysis_server/src/operation/operation.dart'; |
| +import 'package:analysis_server/src/operation/operation_analysis.dart'; |
| +import 'package:analysis_server/src/operation/operation_queue.dart'; |
| +import 'package:analysis_server/src/protocol.dart' hide Element; |
| import 'package:analysis_server/src/socket_server.dart'; |
| +import 'package:analysis_server/src/status/ast_writer.dart'; |
| +import 'package:analysis_server/src/status/element_writer.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| -import 'package:analyzer/instrumentation/instrumentation.dart'; |
| +import 'package:analyzer/src/generated/ast.dart'; |
| +import 'package:analyzer/src/generated/element.dart'; |
| import 'package:analyzer/src/generated/engine.dart'; |
| import 'package:analyzer/src/generated/java_engine.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| @@ -20,6 +29,13 @@ import 'package:analyzer/src/generated/source.dart'; |
| import 'analysis_server.dart'; |
| /** |
| + * A function that can be used to generate HTML output into the given [buffer]. |
| + * The HTML that is generated must be valid (special characters must already be |
| + * encoded). |
| + */ |
| +typedef void HtmlGenerator(StringBuffer buffer); |
| + |
| +/** |
| * Instances of the class [GetHandler] handle GET requests. |
| */ |
| class GetHandler { |
| @@ -29,12 +45,23 @@ class GetHandler { |
| static const String STATUS_PATH = '/status'; |
| /** |
| + * The path used to request information about a element model. |
| + */ |
| + static const String AST_PATH = '/ast'; |
| + |
| + /** |
| * 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 information about the cache entry corresponding |
| + * to a single file. |
| + */ |
| + static const String CACHE_ENTRY2_PATH = '/cache_entry2'; |
| + |
| + /** |
| * The path used to request the list of source files in a certain cache |
| * state. |
| */ |
| @@ -46,6 +73,16 @@ class GetHandler { |
| static const String COMPLETION_PATH = '/completion'; |
| /** |
| + * The path used to request information about a specific context. |
| + */ |
| + static const String CONTEXT_PATH = '/context'; |
| + |
| + /** |
| + * The path used to request information about a element model. |
| + */ |
| + static const String ELEMENT_PATH = '/element'; |
| + |
| + /** |
| * The path used to request an overlay contents. |
| */ |
| static const String OVERLAY_PATH = '/overlay'; |
| @@ -74,6 +111,11 @@ class GetHandler { |
| static const String DESCRIPTOR_QUERY_PARAM = 'descriptor'; |
| /** |
| + * Query parameter used to represent the index in the [_overlayContents]. |
| + */ |
| + static const String ID_PARAM = 'id'; |
| + |
| + /** |
| * Query parameter used to represent the source to search for, when accessing |
| * [CACHE_ENTRY_PATH]. |
| */ |
| @@ -106,12 +148,22 @@ class GetHandler { |
| String path = request.uri.path; |
| if (path == STATUS_PATH) { |
| _returnServerStatus(request); |
| + } else if (path == '/status2') { |
| + _returnServerStatus2(request); |
| + } else if (path == AST_PATH) { |
| + _returnAst(request); |
| } else if (path == CACHE_STATE_PATH) { |
| _returnCacheState(request); |
| } else if (path == CACHE_ENTRY_PATH) { |
| _returnCacheEntry(request); |
| + } else if (path == CACHE_ENTRY2_PATH) { |
| + _returnCacheEntry2(request); |
| } else if (path == COMPLETION_PATH) { |
| _returnCompletionInfo(request); |
| + } else if (path == CONTEXT_PATH) { |
| + _returnContextInfo(request); |
| + } else if (path == ELEMENT_PATH) { |
| + _returnElement(request); |
| } else if (path == OVERLAY_PATH) { |
| _returnOverlayContents(request); |
| } else if (path == OVERLAYS_PATH) { |
| @@ -122,12 +174,52 @@ class GetHandler { |
| } |
| /** |
| + * Return the folder being managed by the given [analysisServer] that matches |
| + * the given [contextFilter], or `null` if there is none. |
| + */ |
| + Folder _findFolder(AnalysisServer analysisServer, String contextFilter) { |
| + return analysisServer.folderMap.keys.firstWhere( |
| + (Folder folder) => folder.path == contextFilter, |
| + orElse: () => null); |
| + } |
| + |
| + /** |
| + * Return `true` if the given analysis [context] has at least one entry with |
| + * an exception. |
| + */ |
| + bool _hasException(AnalysisContextImpl context) { |
| + bool hasException = false; |
| + context.visitCacheItems( |
| + (Source source, SourceEntry sourceEntry, DataDescriptor rowDesc, |
| + CacheState state) { |
| + if (sourceEntry.exception != null) { |
| + hasException = true; |
| + } |
| + }); |
| + return hasException; |
| + } |
| + |
| + Folder _keyForValue(Map<Folder, AnalysisContext> folderMap, |
| + AnalysisContext context) { |
| + for (Folder folder in folderMap.keys) { |
| + if (folderMap[folder] == context) { |
| + return folder; |
| + } |
| + } |
| + return null; |
| + } |
| + |
| + /** |
| * Create a link to [path] with query parameters [params], with inner HTML |
| - * [innerHtml]. |
| + * [innerHtml]. If [hasError] is `true`, then the link will have the class |
| + * 'error'. |
| */ |
| - String _makeLink(String path, Map<String, String> params, String innerHtml) { |
| + String _makeLink(String path, Map<String, String> params, String innerHtml, |
| + [bool hasError = false]) { |
| Uri uri = new Uri(path: path, queryParameters: params); |
| - return '<a href="${HTML_ESCAPE.convert(uri.toString())}">$innerHtml</a>'; |
| + String href = HTML_ESCAPE.convert(uri.toString()); |
| + String classAttribute = hasError ? ' class="error"' : ''; |
| + return '<a href="$href"$classAttribute>$innerHtml</a>'; |
| } |
| /** |
| @@ -180,6 +272,59 @@ class GetHandler { |
| } |
| /** |
| + * Return a response containing information about an AST structure. |
| + */ |
| + void _returnAst(HttpRequest request) { |
| + AnalysisServer analysisServer = _server.analysisServer; |
| + if (analysisServer == null) { |
| + return _returnFailure(request, 'Analysis server not running'); |
| + } |
| + String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; |
| + if (contextFilter == null) { |
| + return _returnFailure( |
| + request, |
| + 'Query parameter $CONTEXT_QUERY_PARAM required'); |
| + } |
| + Folder folder = _findFolder(analysisServer, contextFilter); |
| + if (folder == null) { |
| + return _returnFailure(request, 'Invalid context: $contextFilter'); |
| + } |
| + String sourceUri = request.uri.queryParameters[SOURCE_QUERY_PARAM]; |
| + if (sourceUri == null) { |
| + return _returnFailure( |
| + request, |
| + 'Query parameter $SOURCE_QUERY_PARAM required'); |
| + } |
| + |
| + AnalysisContextImpl context = analysisServer.folderMap[folder]; |
| + |
| + _writeResponse(request, (StringBuffer buffer) { |
| + _writePage( |
| + buffer, |
| + 'Analysis Server - AST Structure', |
| + ['Context: $contextFilter', 'File: $sourceUri'], |
| + (HttpResponse) { |
| + Source source = context.sourceFactory.forUri(sourceUri); |
| + if (source == null) { |
| + buffer.write('<p>Not found.</p>'); |
| + return; |
| + } |
| + SourceEntry entry = context.getReadableSourceEntryOrNull(source); |
| + if (entry == null) { |
| + buffer.write('<p>Not found.</p>'); |
| + return; |
| + } |
| + CompilationUnit ast = (entry as DartEntry).anyParsedCompilationUnit; |
| + if (ast == null) { |
| + buffer.write('<p>null</p>'); |
| + return; |
| + } |
| + ast.accept(new AstWriter(buffer)); |
| + }); |
| + }); |
| + } |
| + |
| + /** |
| * Return a response containing information about a single source file in the |
| * cache. |
| */ |
| @@ -204,6 +349,7 @@ class GetHandler { |
| 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"); |
| @@ -253,10 +399,88 @@ class GetHandler { |
| }); |
| response.write('</body>'); |
| response.write('</html>'); |
| + |
| response.close(); |
| } |
| /** |
| + * Return a response containing information about a single source file in the |
| + * cache. |
| + */ |
| + void _returnCacheEntry2(HttpRequest request) { |
| + AnalysisServer analysisServer = _server.analysisServer; |
| + if (analysisServer == null) { |
| + return _returnFailure(request, 'Analysis server not running'); |
| + } |
| + String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; |
| + if (contextFilter == null) { |
| + return _returnFailure( |
| + request, |
| + 'Query parameter $CONTEXT_QUERY_PARAM required'); |
| + } |
| + Folder folder = _findFolder(analysisServer, contextFilter); |
| + if (folder == null) { |
| + return _returnFailure(request, 'Invalid context: $contextFilter'); |
| + } |
| + String sourceUri = request.uri.queryParameters[SOURCE_QUERY_PARAM]; |
| + if (sourceUri == null) { |
| + return _returnFailure( |
| + request, |
| + 'Query parameter $SOURCE_QUERY_PARAM required'); |
| + } |
| + |
| + AnalysisContextImpl context = analysisServer.folderMap[folder]; |
| + |
| + _writeResponse(request, (StringBuffer buffer) { |
| + _writePage( |
| + buffer, |
| + 'Analysis Server - Cache Entry', |
| + ['Context: $contextFilter', 'File: $sourceUri'], |
| + (HttpResponse) { |
| + Source source = context.sourceFactory.forUri(sourceUri); |
| + if (source == null) { |
| + buffer.write('<p>Not found.</p>'); |
| + return; |
| + } |
| + SourceEntry entry = context.getReadableSourceEntryOrNull(source); |
| + if (entry == null) { |
| + buffer.write('<p>Not found.</p>'); |
| + return; |
| + } |
| + Map<String, String> linkParameters = <String, String>{ |
| + CONTEXT_QUERY_PARAM: folder.path, |
| + SOURCE_QUERY_PARAM: source.uri.toString() |
| + }; |
| + buffer.write('<h3>Library Independent</h3>'); |
| + _writeDescriptorTable( |
| + buffer, |
| + entry.descriptors, |
| + entry.getState, |
| + entry.getValue, |
| + linkParameters); |
| + if (entry is DartEntry) { |
| + for (Source librarySource in entry.containingLibraries) { |
| + String libraryName = HTML_ESCAPE.convert(librarySource.fullName); |
| + buffer.write('<h3>In library $libraryName:</h3>'); |
| + _writeDescriptorTable( |
| + buffer, |
| + entry.libraryDescriptors, |
| + (DataDescriptor descriptor) => |
| + entry.getStateInLibrary(descriptor, librarySource), |
| + (DataDescriptor descriptor) => |
| + entry.getValueInLibrary(descriptor, librarySource), |
| + linkParameters); |
| + } |
| + } |
| + if (entry.exception != null) { |
| + buffer.write('<h3>Exception</h3>'); |
| + _writeException(buffer, entry.exception); |
| + } |
| + }); |
| + }); |
| + } |
| + |
| + /** |
| * Return a response indicating the set of source files in a certain cache |
| * state. |
| */ |
| @@ -376,6 +600,184 @@ class GetHandler { |
| response.close(); |
| } |
| + /** |
| + * Return a response containing information about a single source file in the |
| + * cache. |
| + */ |
| + void _returnContextInfo(HttpRequest request) { |
| + AnalysisServer analysisServer = _server.analysisServer; |
| + if (analysisServer == null) { |
| + return _returnFailure(request, 'Analysis server not running'); |
| + } |
| + String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; |
| + if (contextFilter == null) { |
| + return _returnFailure( |
| + request, |
| + 'Query parameter $CONTEXT_QUERY_PARAM required'); |
| + } |
| + Folder folder = _findFolder(analysisServer, contextFilter); |
| + if (folder == null) { |
| + return _returnFailure(request, 'Invalid context: $contextFilter'); |
| + } |
| + |
| + List<String> priorityNames; |
| + List<String> explicitNames = <String>[]; |
| + List<String> implicitNames = <String>[]; |
| + Map<String, String> links = new HashMap<String, String>(); |
| + List<CaughtException> exceptions = <CaughtException>[]; |
| + Map<String, int> overlayMap = new HashMap<String, int>(); |
| + AnalysisContextImpl context = analysisServer.folderMap[folder]; |
| + priorityNames = |
| + context.prioritySources.map((Source source) => source.fullName).toList(); |
| + context.visitCacheItems( |
| + (Source source, SourceEntry sourceEntry, DataDescriptor rowDesc, |
| + CacheState state) { |
| + String sourceName = source.fullName; |
| + if (!links.containsKey(sourceName)) { |
| + CaughtException exception = sourceEntry.exception; |
| + if (exception != null) { |
| + exceptions.add(exception); |
| + } |
| + String link = _makeLink(CACHE_ENTRY2_PATH, { |
| + CONTEXT_QUERY_PARAM: folder.path, |
| + SOURCE_QUERY_PARAM: source.uri.toString() |
| + }, sourceName, exception != null); |
| + if (sourceEntry.explicitlyAdded) { |
| + explicitNames.add(sourceName); |
| + } else { |
| + implicitNames.add(sourceName); |
| + } |
| + links[sourceName] = link; |
| + } |
| + }); |
| + _overlayContents.clear(); |
| + int count = 0; |
| + context.visitContentCache((Source source, int stamp, String contents) { |
| + count++; |
| + overlayMap[source.fullName] = count; |
| + _overlayContents[count] = contents; |
| + }); |
| + explicitNames.sort(); |
| + implicitNames.sort(); |
| + |
| + void _writeFiles(StringBuffer buffer, String title, List<String> fileNames) |
| + { |
| + buffer.write('<h3>$title</h3>'); |
| + if (fileNames == null || fileNames.isEmpty) { |
| + buffer.write('<p>None</p>'); |
| + } else { |
| + buffer.write('<table style="width: 100%">'); |
| + for (String fileName in fileNames) { |
| + buffer.write('<tr><td>'); |
| + buffer.write(links[fileName]); |
| + buffer.write('</td><td>'); |
| + if (overlayMap.containsKey(fileName)) { |
| + buffer.write(_makeLink(OVERLAY_PATH, { |
| + ID_PARAM: overlayMap[fileName].toString() |
| + }, 'overlay')); |
| + } |
| + buffer.write('</td></tr>'); |
| + } |
| + buffer.write('</table>'); |
| + } |
| + } |
| + |
| + _writeResponse(request, (StringBuffer buffer) { |
| + _writePage( |
| + buffer, |
| + 'Analysis Server - Context', |
| + ['Context: $contextFilter'], |
| + (StringBuffer buffer) { |
| + List headerRowText = ['Context']; |
| + headerRowText.addAll(CacheState.values); |
| + buffer.write('<h3>Summary</h3>'); |
| + buffer.write('<table>'); |
| + _writeRow2(buffer, headerRowText, header: true); |
| + AnalysisContextStatistics statistics = context.statistics; |
| + 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)); |
| + } |
| + _writeRow2(buffer, rowText, classes: [null, "right"]); |
| + }); |
| + buffer.write('</table>'); |
| + |
| + _writeFiles(buffer, 'Priority Files', priorityNames); |
| + _writeFiles(buffer, 'Explicitly Analyzed Files', explicitNames); |
| + _writeFiles(buffer, 'Implicitly Analyzed Files', implicitNames); |
| + |
| + buffer.write('<h3>Exceptions</h3>'); |
| + if (exceptions.isEmpty) { |
| + buffer.write('<p>None</p>'); |
| + } else { |
| + exceptions.forEach((CaughtException exception) { |
| + _writeException(buffer, exception); |
| + }); |
| + } |
| + }); |
| + }); |
| + } |
| + |
| + /** |
| + * Return a response containing information about an element structure. |
| + */ |
| + void _returnElement(HttpRequest request) { |
| + AnalysisServer analysisServer = _server.analysisServer; |
| + if (analysisServer == null) { |
| + return _returnFailure(request, 'Analysis server not running'); |
| + } |
| + String contextFilter = request.uri.queryParameters[CONTEXT_QUERY_PARAM]; |
| + if (contextFilter == null) { |
| + return _returnFailure( |
| + request, |
| + 'Query parameter $CONTEXT_QUERY_PARAM required'); |
| + } |
| + Folder folder = _findFolder(analysisServer, contextFilter); |
| + if (folder == null) { |
| + return _returnFailure(request, 'Invalid context: $contextFilter'); |
| + } |
| + String sourceUri = request.uri.queryParameters[SOURCE_QUERY_PARAM]; |
| + if (sourceUri == null) { |
| + return _returnFailure( |
| + request, |
| + 'Query parameter $SOURCE_QUERY_PARAM required'); |
| + } |
| + |
| + AnalysisContextImpl context = analysisServer.folderMap[folder]; |
| + |
| + _writeResponse(request, (StringBuffer buffer) { |
| + _writePage( |
| + buffer, |
| + 'Analysis Server - Element Model', |
| + ['Context: $contextFilter', 'File: $sourceUri'], |
| + (HttpResponse) { |
| + Source source = context.sourceFactory.forUri(sourceUri); |
| + if (source == null) { |
| + buffer.write('<p>Not found.</p>'); |
| + return; |
| + } |
| + SourceEntry entry = context.getReadableSourceEntryOrNull(source); |
| + if (entry == null) { |
| + buffer.write('<p>Not found.</p>'); |
| + return; |
| + } |
| + LibraryElement element = entry.getValue(DartEntry.ELEMENT); |
| + if (element == null) { |
| + buffer.write('<p>null</p>'); |
| + return; |
| + } |
| + element.accept(new ElementWriter(buffer)); |
| + }); |
| + }); |
| + } |
| + |
| void _returnFailure(HttpRequest request, String message) { |
| HttpResponse response = request.response; |
| response.statusCode = HttpStatus.OK; |
| @@ -392,7 +794,7 @@ class GetHandler { |
| } |
| void _returnOverlayContents(HttpRequest request) { |
| - String idString = request.requestedUri.queryParameters['id']; |
| + String idString = request.requestedUri.queryParameters[ID_PARAM]; |
| int id = int.parse(idString); |
| String contents = _overlayContents[id]; |
| HttpResponse response = request.response; |
| @@ -538,7 +940,9 @@ class GetHandler { |
| if (!exceptions.isEmpty) { |
| response.write('<h2>Exceptions</h2>'); |
| exceptions.forEach((CaughtException exception) { |
| - response.write('<p>${exception.exception}</p>'); |
| + StringBuffer buffer = new StringBuffer(); |
| + _writeException(buffer, exception); |
| + response.write(buffer.toString()); |
| }); |
| } |
| }); |
| @@ -553,6 +957,22 @@ class GetHandler { |
| } |
| /** |
| + * Return a response indicating the status of the analysis server. |
| + */ |
| + void _returnServerStatus2(HttpRequest request) { |
| + _writeResponse(request, (StringBuffer buffer) { |
| + _writePage(buffer, 'Analysis Server - Status', [], (StringBuffer buffer) { |
| + if (_writeServerStatus(buffer)) { |
| + _writeAnalysisStatus(buffer); |
| + _writeEditStatus(buffer); |
| + _writeExecutionStatus(buffer); |
| + _writeRecentOutput(buffer); |
| + } |
| + }); |
| + }); |
| + } |
| + |
| + /** |
| * Return an error in response to an unrecognized request received by the HTTP |
| * server. |
| */ |
| @@ -577,6 +997,93 @@ class GetHandler { |
| } |
| /** |
| + * Return a two digit decimal representation of the given integer [value]. |
|
Paul Berry
2015/01/20 17:28:11
Nit: this function will produce nonsense if the in
Brian Wilkerson
2015/01/20 17:46:34
Done.
|
| + */ |
| + String _twoDigit(int value) { |
| + if (value < 10) { |
| + return '0$value'; |
| + } |
| + return value.toString(); |
| + } |
| + |
| + /** |
| + * Write the status of the analysis domain (on the main status page) to the |
| + * given [buffer] object. |
| + */ |
| + void _writeAnalysisStatus(StringBuffer buffer) { |
| + AnalysisServer analysisServer = _server.analysisServer; |
| + Map<Folder, AnalysisContext> folderMap = analysisServer.folderMap; |
| + List<Folder> folders = folderMap.keys.toList(); |
| + folders.sort( |
| + (Folder first, Folder second) => first.shortName.compareTo(second.shortName)); |
| + AnalysisOptionsImpl options = |
| + analysisServer.contextDirectoryManager.defaultOptions; |
| + ServerOperationQueue operationQueue = analysisServer.operationQueue; |
| + |
| + buffer.write('<h3>Analysis Domain</h3>'); |
| + _writeTwoColumns(buffer, (StringBuffer buffer) { |
| + if (operationQueue.isEmpty) { |
| + buffer.write('<p>Status: Done analyzing</p>'); |
| + } else { |
| + ServerOperation operation = operationQueue.peek(); |
| + if (operation is PerformAnalysisOperation) { |
| + Folder folder = _keyForValue(folderMap, operation.context); |
| + if (folder == null) { |
| + buffer.write('<p>Status: Analyzing in unmapped context</p>'); |
| + } else { |
| + buffer.write('<p>Status: Analyzing in ${folder.path}</p>'); |
| + } |
| + } else { |
| + buffer.write('<p>Status: Analyzing</p>'); |
| + } |
| + } |
| + |
| + buffer.write('<p><b>Analysis Contexts</b></p>'); |
| + buffer.write('<p>'); |
| + bool first = true; |
| + folders.forEach((Folder folder) { |
| + if (first) { |
| + first = false; |
| + } else { |
| + buffer.write('<br>'); |
| + } |
| + String key = folder.shortName; |
| + buffer.write(_makeLink(CONTEXT_PATH, { |
| + CONTEXT_QUERY_PARAM: folder.path |
| + }, key, _hasException(folderMap[folder]))); |
| + }); |
| + buffer.write('</p>'); |
| + |
| + buffer.write('<p><b>Options</b></p>'); |
| + buffer.write('<p>'); |
| + _writeOption( |
| + buffer, |
| + 'Analyze functon bodies', |
| + options.analyzeFunctionBodies); |
| + _writeOption(buffer, 'Cache size', options.cacheSize); |
| + _writeOption(buffer, 'Generate hints', options.hint); |
| + _writeOption(buffer, 'Generate dart2js hints', options.dart2jsHint); |
| + _writeOption(buffer, 'Generate SDK errors', options.generateSdkErrors); |
| + _writeOption(buffer, 'Incremental resolution', options.incremental); |
| + _writeOption( |
| + buffer, |
| + 'Incremental resolution with API changes', |
| + options.incrementalApi); |
| + _writeOption( |
| + buffer, |
| + 'Preserve comments', |
| + options.preserveComments, |
| + last: true); |
| + buffer.write('</p>'); |
| + }, (StringBuffer buffer) { |
| + _writeSubscriptionMap( |
| + buffer, |
| + AnalysisService.VALUES, |
| + analysisServer.analysisServices); |
| + }); |
| + } |
| + |
| + /** |
| * Append code completion information. |
| */ |
| void _writeCompletionInfo(HttpResponse response, maxCount) { |
| @@ -662,6 +1169,220 @@ class GetHandler { |
| } |
| /** |
| + * 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 the given [buffer]. The |
| + * [linkParameters] will be used if the value is too large to be displayed on |
| + * the current page and needs to be linked to a separate page. |
| + */ |
| + void _writeDescriptorTable(StringBuffer buffer, |
| + List<DataDescriptor> descriptors, CacheState getState(DataDescriptor), dynamic |
| + getValue(DataDescriptor), Map<String, String> linkParameters) { |
| + buffer.write('<dl>'); |
| + for (DataDescriptor descriptor in descriptors) { |
| + String descriptorName = HTML_ESCAPE.convert(descriptor.toString()); |
| + String descriptorState = |
| + HTML_ESCAPE.convert(getState(descriptor).toString()); |
| + buffer.write('<dt>$descriptorName ($descriptorState)</dt><dd>'); |
| + try { |
| + _writeValueAsHtml(buffer, getValue(descriptor), linkParameters); |
| + } catch (exception) { |
| + buffer.write('(${HTML_ESCAPE.convert(exception.toString())})'); |
| + } |
| + buffer.write('</dd>'); |
| + } |
| + buffer.write('</dl>'); |
| + } |
| + |
| + /** |
| + * Write the status of the edit domain (on the main status page) to the given |
| + * [buffer]. |
| + */ |
| + void _writeEditStatus(StringBuffer buffer) { |
| + buffer.write('<h3>Edit Domain</h3>'); |
| + _writeTwoColumns(buffer, (StringBuffer buffer) { |
| + buffer.write('<p><b>Performance Data</b></p>'); |
| + buffer.write('<p>'); |
| + buffer.write(_makeLink(COMPLETION_PATH, {}, 'Completion data')); |
| + buffer.write('</p>'); |
| + }, (StringBuffer buffer) { |
| + }); |
| + } |
| + |
| + /** |
| + * Write a representation of the given [caughtException] to the given |
| + * [buffer]. If [isCause] is `true`, then the exception was a cause for |
| + * another exception. |
| + */ |
| + void _writeException(StringBuffer buffer, CaughtException caughtException, |
| + {bool isCause: false}) { |
| + Object exception = caughtException.exception; |
| + |
| + if (exception is AnalysisException) { |
| + buffer.write('<p>'); |
| + if (isCause) { |
| + buffer.write('Caused by '); |
| + } |
| + buffer.write(exception.message); |
| + buffer.write('</p>'); |
| + _writeStackTrace(buffer, caughtException.stackTrace); |
| + CaughtException cause = exception.cause; |
| + if (cause != null) { |
| + buffer.write('<blockquote>'); |
| + _writeException(buffer, cause, isCause: true); |
| + buffer.write('</blockquote>'); |
| + } |
| + } else { |
| + buffer.write('<p>'); |
| + if (isCause) { |
| + buffer.write('Caused by '); |
| + } |
| + buffer.write(exception.toString()); |
| + buffer.write('<p>'); |
| + _writeStackTrace(buffer, caughtException.stackTrace); |
| + } |
| + } |
| + |
| + /** |
| + * Write the status of the execution domain (on the main status page) to the |
| + * given [buffer]. |
| + */ |
| + void _writeExecutionStatus(StringBuffer buffer) { |
| + AnalysisServer analysisServer = _server.analysisServer; |
| + ExecutionDomainHandler handler = analysisServer.handlers.firstWhere( |
| + (RequestHandler handler) => handler is ExecutionDomainHandler, |
| + orElse: () => null); |
| + Set<ExecutionService> services = new Set<ExecutionService>(); |
| + if (handler.onFileAnalyzed != null) { |
| + services.add(ExecutionService.LAUNCH_DATA); |
| + } |
| + |
| + if (handler != null) { |
| + buffer.write('<h3>Execution Domain</h3>'); |
| + _writeTwoColumns(buffer, (StringBuffer buffer) { |
| + _writeSubscriptionList(buffer, ExecutionService.VALUES, services); |
| + }, (StringBuffer buffer) { |
| + }); |
| + } |
| + } |
| + |
| + /** |
| + * Write a representation of an analysis option with the given [name] and |
| + * [value] to the given [buffer]. The option should be separated from other |
| + * options unless the [last] flag is true, indicating that this is the last |
| + * option in the list of options. |
| + */ |
| + void _writeOption(StringBuffer buffer, String name, Object value, {bool last: |
| + false}) { |
| + buffer.write(name); |
| + buffer.write(' = '); |
| + buffer.write(value.toString()); |
| + if (!last) { |
| + buffer.write('<br>'); |
| + } |
| + } |
| + |
| + /** |
| + * Write a standard HTML page to the given [buffer]. The page will have the |
| + * given [title] and a body that is generated by the given [body] generator. |
| + */ |
| + void _writePage(StringBuffer buffer, String title, List<String> subtitles, |
| + HtmlGenerator body) { |
| + DateTime now = new DateTime.now(); |
| + String date = "${now.month}/${now.day}/${now.year}"; |
| + String time = |
| + "${now.hour}:${_twoDigit(now.minute)}:${_twoDigit(now.second)}.${now.millisecond}"; |
| + |
| + buffer.write('<!DOCTYPE html>'); |
| + buffer.write('<html>'); |
| + buffer.write('<head>'); |
| + buffer.write('<meta charset="utf-8">'); |
| + buffer.write( |
| + '<meta name="viewport" content="width=device-width, initial-scale=1.0">'); |
| + buffer.write('<title>$title</title>'); |
| + buffer.write('<style>'); |
| + buffer.write('a {color: #0000DD; text-decoration: none;}'); |
| + buffer.write('a:link.error {background-color: #FFEEEE;}'); |
| + buffer.write('a:visited.error {background-color: #FFEEEE;}'); |
| + buffer.write('a:hover.error {background-color: #FFEEEE;}'); |
| + buffer.write('a:active.error {background-color: #FFEEEE;}'); |
| + buffer.write( |
| + 'h3 {background-color: #DDDDDD; margin-top: 0em; margin-bottom: 0em;}'); |
| + buffer.write('p {margin-top: 0.5em; margin-bottom: 0.5em;}'); |
| +// response.write('span.error {text-decoration-line: underline; text-decoration-color: red; text-decoration-style: wavy;}'); |
| + buffer.write( |
| + 'table.column {border: 0px solid black; width: 100%; table-layout: fixed;}'); |
| + buffer.write('td.column {vertical-align: top; width: 50%;}'); |
| + buffer.write('td.right {text-align: right;}'); |
| + buffer.write('</style>'); |
| + buffer.write('</head>'); |
| + |
| + buffer.write('<body>'); |
| + buffer.write( |
| + '<h2>$title <small><small>(as of $time on $date)</small></small></h2>'); |
| + if (subtitles != null && subtitles.isNotEmpty) { |
| + buffer.write('<blockquote>'); |
| + bool first = true; |
| + for (String subtitle in subtitles) { |
| + if (first) { |
| + first = false; |
| + } else { |
| + buffer.write('<br>'); |
| + } |
| + buffer.write('<b>'); |
| + buffer.write(subtitle); |
| + buffer.write('</b>'); |
| + } |
| + buffer.write('</blockquote>'); |
| + } |
| + try { |
| + body(buffer); |
| + } catch (exception, stackTrace) { |
| + buffer.write('<h3>Exception while creating page</h3>'); |
| + _writeException(buffer, new CaughtException(exception, stackTrace)); |
| + } |
| + buffer.write('</body>'); |
| + buffer.write('</html>'); |
| + } |
| + |
| + /** |
| + * Write the recent output section (on the main status page) to the given |
| + * [buffer] object. |
| + */ |
| + void _writeRecentOutput(StringBuffer buffer) { |
| + buffer.write('<h3>Recent Output</h3>'); |
| + String output = HTML_ESCAPE.convert(_printBuffer.join('\n')); |
| + if (output.isEmpty) { |
| + buffer.write('<i>none</i>'); |
| + } else { |
| + buffer.write('<pre>'); |
| + buffer.write(output); |
| + buffer.write('</pre>'); |
| + } |
| + } |
| + |
| + void _writeResponse(HttpRequest request, HtmlGenerator writePage) { |
| + HttpResponse response = request.response; |
| + response.statusCode = HttpStatus.OK; |
| + response.headers.add(HttpHeaders.CONTENT_TYPE, "text/html"); |
| + try { |
| + StringBuffer buffer = new StringBuffer(); |
| + try { |
| + writePage(buffer); |
| + } catch (exception, stackTrace) { |
| + buffer.clear(); |
| + _writePage(buffer, 'Internal Exception', [], (StringBuffer buffer) { |
| + _writeException(buffer, new CaughtException(exception, stackTrace)); |
| + }); |
| + } |
| + response.write(buffer.toString()); |
| + } finally { |
| + response.close(); |
| + } |
| + } |
| + |
| + /** |
| * 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`. |
| @@ -694,4 +1415,202 @@ class GetHandler { |
| response.write('</tr>'); |
| } |
| + |
| + /** |
| + * Write a single row within a table to the given [buffer]. The row will have |
| + * one cell for each of the [columns], and will be a header row if [header] is |
| + * `true`. |
| + */ |
| + void _writeRow2(StringBuffer buffer, List<Object> columns, {bool header: |
| + false, List<String> classes}) { |
| + buffer.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) { |
| + buffer.write('<th$classAttribute>'); |
| + } else { |
| + buffer.write('<td$classAttribute>'); |
| + } |
| + buffer.write(columns[i]); |
| + if (header) { |
| + buffer.write('</th>'); |
| + } else { |
| + buffer.write('</td>'); |
| + } |
| + } |
| + buffer.write('</tr>'); |
| + } |
| + |
| + /** |
| + * Write the status of the service domain (on the main status page) to the |
| + * given [response] object. |
| + */ |
| + bool _writeServerStatus(StringBuffer buffer) { |
| + AnalysisServer analysisServer = _server.analysisServer; |
| + Set<ServerService> services = analysisServer.serverServices; |
| + |
| + buffer.write('<h3>Server Domain</h3>'); |
| + _writeTwoColumns(buffer, (StringBuffer buffer) { |
| + if (analysisServer == null) { |
| + buffer.write('Status: <span style="color:red">Not running</span>'); |
| + return false; |
| + } |
| + buffer.write('<p>'); |
| + buffer.write('Status: Running<br>'); |
| + buffer.write('Instrumentation: '); |
| + if (AnalysisEngine.instance.instrumentationService.isActive) { |
| + buffer.write('<span style="color:red">Active</span>'); |
| + } else { |
| + buffer.write('Inactive'); |
| + } |
| + buffer.write('<br>'); |
| + buffer.write('Version: '); |
| + buffer.write(AnalysisServer.VERSION); |
| + buffer.write('</p>'); |
| + }, (StringBuffer buffer) { |
| + _writeSubscriptionList(buffer, ServerService.VALUES, services); |
| + }); |
| + return true; |
| + } |
| + |
| + /** |
| + * Write a representation of the given [stackTrace] to the given [buffer]. |
| + */ |
| + void _writeStackTrace(StringBuffer buffer, StackTrace stackTrace) { |
| + if (stackTrace != null) { |
| + String trace = stackTrace.toString().replaceAll('#', '<br>#'); |
| + if (trace.startsWith('<br>#')) { |
| + trace = trace.substring(4); |
| + } |
| + buffer.write('<p>'); |
| + buffer.write(trace); |
| + buffer.write('</p>'); |
| + } |
| + } |
| + |
| + /** |
| + * Given a [service] that could be subscribed to and a set of the services |
| + * that are actually subscribed to ([subscribedServices]), write a |
| + * representation of the service to the given [buffer]. |
| + */ |
| + void _writeSubscriptionInList(StringBuffer buffer, Enum service, |
| + Set<Enum> subscribedServices) { |
| + if (subscribedServices.contains(service)) { |
| + buffer.write('<code>+ </code>'); |
| + } else { |
| + buffer.write('<code>- </code>'); |
| + } |
| + buffer.write(service.name); |
| + buffer.write('<br>'); |
| + } |
| + |
| + /** |
| + * Given a [service] that could be subscribed to and a set of paths that are |
| + * subscribed to the services ([subscribedPaths]), write a representation of |
| + * the service to the given [buffer]. |
| + */ |
| + void _writeSubscriptionInMap(StringBuffer buffer, Enum service, |
| + Set<String> subscribedPaths) { |
| + buffer.write('<p>'); |
| + buffer.write(service.name); |
| + buffer.write('</p>'); |
| + if (subscribedPaths == null || subscribedPaths.isEmpty) { |
| + buffer.write('none'); |
| + } else { |
| + List<String> paths = subscribedPaths.toList(); |
| + paths.sort(); |
| + for (String path in paths) { |
| + buffer.write('<p>'); |
| + buffer.write(path); |
| + buffer.write('</p>'); |
| + } |
| + } |
| + } |
| + |
| + /** |
| + * Given a list containing all of the services that can be subscribed to in a |
| + * single domain ([allServices]) and a set of the services that are actually |
| + * subscribed to ([subscribedServices]), write a representation of the |
| + * subscriptions to the given [buffer]. |
| + */ |
| + void _writeSubscriptionList(StringBuffer buffer, List<Enum> allServices, |
| + Set<Enum> subscribedServices) { |
| + buffer.write('<p><b>Subscriptions</b></p>'); |
| + buffer.write('<p>'); |
| + for (Enum service in allServices) { |
| + _writeSubscriptionInList(buffer, service, subscribedServices); |
| + } |
| + buffer.write('</p>'); |
| + } |
| + |
| + /** |
| + * Given a list containing all of the services that can be subscribed to in a |
| + * single domain ([allServices]) and a set of the services that are actually |
| + * subscribed to ([subscribedServices]), write a representation of the |
| + * subscriptions to the given [buffer]. |
| + */ |
| + void _writeSubscriptionMap(StringBuffer buffer, List<Enum> allServices, |
| + Map<Enum, Set<String>> subscribedServices) { |
| + buffer.write('<p><b>Subscriptions</b></p>'); |
| + for (Enum service in allServices) { |
| + _writeSubscriptionInMap(buffer, service, subscribedServices[service]); |
| + } |
| + } |
| + |
| + /** |
| + * Write two columns of information to the given [buffer], where the |
| + * [leftColumn] and [rightColumn] functions are used to generate the content |
| + * of those columns. |
| + */ |
| + void _writeTwoColumns(StringBuffer buffer, HtmlGenerator leftColumn, |
| + HtmlGenerator rightColumn) { |
| + buffer.write( |
| + '<table class="column"><tr class="column"><td class="column">'); |
| + leftColumn(buffer); |
| + buffer.write('</td><td class="column">'); |
| + rightColumn(buffer); |
| + buffer.write('</td></tr></table>'); |
| + } |
| + |
| + /** |
| + * Render the given [value] as HTML and append it to the given [buffer]. The |
| + * [linkParameters] will be used if the value is too large to be displayed on |
| + * the current page and needs to be linked to a separate page. |
| + */ |
| + void _writeValueAsHtml(StringBuffer buffer, Object value, Map<String, |
| + String> linkParameters) { |
| + if (value == null) { |
| + buffer.write('<i>null</i>'); |
| + } else if (value is String) { |
| + buffer.write('<pre>${HTML_ESCAPE.convert(value)}</pre>'); |
| + } else if (value is List) { |
| + buffer.write('List containing ${value.length} entries'); |
| + buffer.write('<ul>'); |
| + for (var entry in value) { |
| + buffer.write('<li>'); |
| + _writeValueAsHtml(buffer, entry, linkParameters); |
| + buffer.write('</li>'); |
| + } |
| + buffer.write('</ul>'); |
| + } else if (value is AstNode) { |
| + String link = |
| + _makeLink(AST_PATH, linkParameters, value.runtimeType.toString()); |
| + buffer.write('<i>$link</i>'); |
| + } else if (value is Element) { |
| + String link = |
| + _makeLink(ELEMENT_PATH, linkParameters, value.runtimeType.toString()); |
| + buffer.write('<i>$link</i>'); |
| + } else { |
| + buffer.write(HTML_ESCAPE.convert(value.toString())); |
| + buffer.write(' <i>(${value.runtimeType.toString()})</i>'); |
| + } |
| + } |
| } |