Chromium Code Reviews| Index: pkg/analysis_server/bin/fuzz/server_manager.dart |
| diff --git a/pkg/analysis_server/bin/fuzz/server_manager.dart b/pkg/analysis_server/bin/fuzz/server_manager.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..eba10d7c9b15dacebbbb21391355f47faae88a82 |
| --- /dev/null |
| +++ b/pkg/analysis_server/bin/fuzz/server_manager.dart |
| @@ -0,0 +1,346 @@ |
| +// 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 server.manager; |
| + |
| +import 'dart:async'; |
| +import 'dart:convert'; |
| +import 'dart:io'; |
| + |
| +import 'package:matcher/matcher.dart'; |
| + |
| +import 'byte_stream_channel.dart'; |
| +import 'channel.dart'; |
| +import 'protocol.dart'; |
| + |
| +part 'logging_client_channel.dart'; |
| + |
| +/** |
| + * The results returned by [ServerManager].analyze(...) once analysis |
| + * has finished. |
| + */ |
| +class AnalysisResults { |
| + Duration elapsed; |
| + int errorCount = 0; |
| + int hintCount = 0; |
| + int warningCount = 0; |
| +} |
| + |
| + |
| +/** |
| + * [CompletionResults] contains the completion results returned by the server |
| + * along with the elapse time to receive those completions. |
| + */ |
| +class CompletionResults { |
| + final Duration elapsed; |
| + final CompletionResultsParams params; |
| + |
| + CompletionResults(this.elapsed, this.params); |
| + |
| + int get suggestionCount => params.results.length; |
| +} |
| + |
| +/** |
| + * [Editor] is a virtual editor for inspecting and modifying a file's content |
| + * and updating the server with those modifications. |
| + */ |
| +class Editor { |
| + final ServerManager manager; |
| + final File file; |
| + int offset = 0; |
| + String _content = null; |
| + |
| + Editor(this.manager, this.file); |
| + |
| + /// Return a future that returns the file content |
| + Future<String> get content { |
| + if (_content != null) { |
| + return new Future.value(_content); |
| + } |
| + return file.readAsString().then((String content) { |
| + _content = content; |
| + return _content; |
| + }); |
| + } |
| + |
| + /** |
| + * Request completion suggestions from the server. |
| + * Return a future that completes with the completions sent. |
| + */ |
| + Future<List<CompletionResults>> getSuggestions() { |
| + Request request = new CompletionGetSuggestionsParams( |
| + file.path, |
| + offset).toRequest(manager._nextId); |
| + Stopwatch stopwatch = new Stopwatch()..start(); |
| + return manager.channel.sendRequest(request).then((Response response) { |
| + var id = new CompletionGetSuggestionsResult.fromResponse(response).id; |
|
lukechurch
2014/09/21 05:36:11
It's not quite obvious from just reading the code
danrubel
2014/09/22 18:27:25
Good point. This is the completionId not the respo
|
| + var completer = new Completer<List<CompletionResults>>(); |
| + List<CompletionResults> results = []; |
| + |
| + // Listen for completion suggestions |
| + StreamSubscription<Notification> subscription; |
| + subscription = |
| + manager.channel.notificationStream.listen((Notification notification) { |
| + if (notification.event == 'completion.results') { |
| + CompletionResultsParams params = |
| + new CompletionResultsParams.fromNotification(notification); |
| + results.add(new CompletionResults(stopwatch.elapsed, params)); |
| + if (params.isLast) { |
| + stopwatch.stop(); |
| + subscription.cancel(); |
| + completer.complete(results); |
| + } |
| + } |
| + }); |
| + |
| + return completer.future; |
| + }); |
| + } |
| + |
| + /** |
| + * Move the virtual cursor after the given pattern in the source. |
| + * Return a future that completes once the cursor has been moved. |
| + */ |
| + Future<Editor> moveAfter(String pattern) { |
| + return content.then((String content) { |
| + offset = content.indexOf(pattern); |
| + return this; |
| + }); |
| + } |
| + |
| + /** |
| + * Replace the specified number of characters at the current cursor location |
| + * with the given text, but do not save that content to disk. |
| + * Return a future that completes once the server has been notified. |
| + */ |
| + Future<Editor> replace(int replacementLength, String text) { |
| + return content.then((String oldContent) { |
| + StringBuffer sb = new StringBuffer(); |
| + sb.write(oldContent.substring(0, offset)); |
| + sb.write(text); |
| + sb.write(oldContent.substring(offset)); |
| + _content = sb.toString(); |
| + SourceEdit sourceEdit = new SourceEdit(offset, replacementLength, text); |
| + Request request = new AnalysisUpdateContentParams({ |
| + file.path: new ChangeContentOverlay([sourceEdit]) |
| + }).toRequest(manager._nextId); |
| + offset += text.length; |
| + return manager.channel.sendRequest(request).then((Response response) { |
| + return this; |
| + }); |
| + }); |
| + } |
| +} |
| + |
| +/** |
| + * [ServerManager] is used to launch and manage an analysis server |
| + * running in a separate process. |
| + */ |
| +class ServerManager { |
| + |
| + /** |
| + * The analysis server process being managed or `null` if not started. |
| + */ |
| + Process process; |
| + |
| + /** |
| + * The channel used to communicate with the analysis server. |
| + */ |
| + LoggingClientChannel _channel; |
| + |
| + bool _unreportedServerException = false; |
| + |
| + bool _stopRequested = false; |
|
lukechurch
2014/09/21 05:36:11
whitespace is a bit whacky
danrubel
2014/09/22 18:27:25
Added comments. Hopefully not so whacky now.
|
| + |
| + Directory appDir; |
| + |
| + int _id = 0; |
| + |
| + /** |
| + * Return the channel used to communicate with the analysis server. |
| + */ |
| + ClientCommunicationChannel get channel => _channel; |
| + |
| + /** |
| + * Return `true` if a server error occurred. |
| + */ |
| + bool get errorOccurred => |
| + _unreportedServerException || (_channel.serverErrorCount > 0); |
| + |
| + String get _nextId => (++_id).toString(); |
| + |
| + /** |
| + * Direct the server to analyze all sources in the given directory. |
| + * Return a future that completes when the analysis is finished. |
|
lukechurch
2014/09/21 05:36:11
Does this recurse into children? Given the ambigui
danrubel
2014/09/22 18:27:26
It analyzes all of the files in the given director
|
| + */ |
| + Future<AnalysisResults> analyze(Directory appDir) { |
| + this.appDir = appDir; |
| + Stopwatch stopwatch = new Stopwatch()..start(); |
| + Request request = |
| + new AnalysisSetAnalysisRootsParams([appDir.path], []).toRequest(_nextId); |
| + |
| + // Request analysis |
| + return channel.sendRequest(request).then((Response response) { |
| + AnalysisResults results = new AnalysisResults(); |
| + StreamSubscription<Notification> subscription; |
| + Completer<AnalysisResults> completer = new Completer<AnalysisResults>(); |
| + subscription = |
| + channel.notificationStream.listen((Notification notification) { |
| + |
| + // Gather analysis results |
| + if (notification.event == 'analysis.errors') { |
| + AnalysisErrorsParams params = |
| + new AnalysisErrorsParams.fromNotification(notification); |
| + params.errors.forEach((AnalysisError error) { |
| + AnalysisErrorSeverity severity = error.severity; |
| + if (severity == AnalysisErrorSeverity.ERROR) { |
| + results.errorCount += 1; |
| + } else if (severity == AnalysisErrorSeverity.WARNING) { |
| + results.warningCount += 1; |
| + } else if (severity == AnalysisErrorSeverity.INFO) { |
| + results.hintCount += 1; |
| + } else { |
| + print('Unknown error severity: ${severity.name}'); |
| + } |
| + }); |
| + } |
| + |
| + // Stop gathering once analysis is complete |
| + if (notification.event == 'server.status') { |
| + ServerStatusParams status = |
| + new ServerStatusParams.fromNotification(notification); |
| + AnalysisStatus analysis = status.analysis; |
| + if (analysis != null && !analysis.isAnalyzing) { |
| + stopwatch.stop(); |
| + results.elapsed = stopwatch.elapsed; |
| + subscription.cancel(); |
| + completer.complete(results); |
| + } |
| + } |
| + }); |
| + return completer.future; |
| + }); |
| + } |
| + |
| + /** |
| + * Send a request to the server for its version information |
| + * and return a future that completes with the result. |
| + */ |
| + Future<ServerGetVersionResult> getVersion() { |
| + Request request = new ServerGetVersionParams().toRequest(_nextId); |
| + return channel.sendRequest(request).then((Response response) { |
| + return new ServerGetVersionResult.fromResponse(response); |
| + }); |
| + } |
| + |
| + /** |
| + * Notify the server that the given file will be edited. |
| + * Return a virtual editor for inspecting and modifying the file's content. |
| + */ |
| + Future<Editor> openFileNamed(String fileName) { |
| + return _findFile(fileName, appDir).then((File file) { |
| + if (file == null) { |
| + throw 'Failed to find file named $fileName in ${appDir.path}'; |
| + } |
| + file = file.absolute; |
| + Request request = |
| + new AnalysisSetPriorityFilesParams([file.path]).toRequest(_nextId); |
| + return channel.sendRequest(request).then((Response response) { |
| + return new Editor(this, file); |
| + }); |
| + }); |
| + } |
| + |
| + /** |
| + * Send a request for notifications. |
| + * Return when the server has acknowledged that request. |
| + */ |
| + Future setSubscriptions() { |
| + Request request = |
| + new ServerSetSubscriptionsParams([ServerService.STATUS]).toRequest(_nextId); |
| + return channel.sendRequest(request); |
| + } |
| + |
| + /** |
| + * Stop the analysis server. |
| + * Return a future that completes when the server is terminated. |
| + */ |
| + Future stop([_]) { |
| + _stopRequested = true; |
| + print("Requesting server shutdown"); |
| + Request request = new ServerShutdownParams().toRequest(_nextId); |
| + Duration waitTime = new Duration(seconds: 5); |
| + return channel.sendRequest(request).timeout(waitTime, onTimeout: () { |
| + print('Expected shutdown response'); |
| + }).then((Response response) { |
| + return channel.close().then((_) => process.exitCode); |
| + }).timeout(new Duration(seconds: 2), onTimeout: () { |
| + print('Expected server to shutdown'); |
| + process.kill(); |
| + }); |
| + } |
| + |
| + /** |
| + * Locate the given file in the directory tree. |
| + */ |
| + Future<File> _findFile(String fileName, Directory appDir) { |
| + return appDir.list(recursive: true).firstWhere((FileSystemEntity entity) { |
| + return entity is File && entity.path.endsWith(fileName); |
| + }); |
| + } |
| + |
| + /** |
| + * Launch an analysis server and open a connection to that server. |
| + */ |
| + Future<ServerManager> _launchServer(String pathToServer) { |
| + List<String> serverArgs = [pathToServer]; |
| + return Process.start(Platform.executable, serverArgs).catchError((error) { |
| + exitCode = 21; |
| + throw 'Failed to launch analysis server: $error'; |
| + }).then((Process process) { |
| + this.process = process; |
| + _channel = new LoggingClientChannel( |
| + new ByteStreamClientChannel(process.stdout, process.stdin)); |
| + |
| + // simple out of band exception handling |
| + process.stderr.transform( |
| + new Utf8Codec().decoder).transform(new LineSplitter()).listen((String line) { |
| + if (!_unreportedServerException) { |
| + _unreportedServerException = true; |
| + stderr.writeln('>>> Unreported server exception'); |
| + } |
| + stderr.writeln('server.stderr: $line'); |
| + }); |
| + |
| + // watch for unexpected process termination and catch the exit code |
| + process.exitCode.then((int code) { |
| + if (!_stopRequested) { |
| + fail('Unexpected server termination: $code'); |
| + } |
| + if (code != null && code != 0) { |
| + exitCode = code; |
| + } |
| + print('Server stopped: $code'); |
| + }); |
| + |
| + return channel.notificationStream.first.then((Notification notification) { |
| + print('Server connection established'); |
| + return setSubscriptions().then((_) { |
| + return getVersion().then((ServerGetVersionResult result) { |
| + print('Server version ${result.version}'); |
| + return this; |
| + }); |
| + }); |
| + }); |
| + }); |
| + } |
| + |
| + /** |
| + * Launch analysis server in a separate process |
| + * and return a future with a manager for that analysis server. |
| + */ |
| + static Future<ServerManager> start(String serverPath) { |
| + return new ServerManager()._launchServer(serverPath); |
| + } |
| +} |