diff --git a/.packages b/.packages index 870088f9563..683de984a97 100644 --- a/.packages +++ b/.packages @@ -7,6 +7,7 @@ # Please update this file if you add a package to DEPS or /pkg # analysis_server:pkg/analysis_server/lib +analysis_server_client:pkg/analysis_server_client/lib analyzer:pkg/analyzer/lib analyzer_cli:pkg/analyzer_cli/lib analyzer_plugin:pkg/analyzer_plugin/lib diff --git a/pkg/analysis_server_client/CHANGELOG.md b/pkg/analysis_server_client/CHANGELOG.md new file mode 100644 index 00000000000..14d2f07d088 --- /dev/null +++ b/pkg/analysis_server_client/CHANGELOG.md @@ -0,0 +1,5 @@ +#1.0.1 +* Update path on homepage + +#1.0.0 +* Initial version \ No newline at end of file diff --git a/pkg/analysis_server_client/LICENSE b/pkg/analysis_server_client/LICENSE new file mode 100644 index 00000000000..076334f7ab7 --- /dev/null +++ b/pkg/analysis_server_client/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pkg/analysis_server_client/README.md b/pkg/analysis_server_client/README.md new file mode 100644 index 00000000000..3351705598b --- /dev/null +++ b/pkg/analysis_server_client/README.md @@ -0,0 +1,13 @@ +# Analysis Server Client + +A client wrapper over Analysis Server. Instances of this client manages +connection to Analysis Server and process and faciliates JSON protocol +communication to and from the server. + +Current implementation has no knowledge of the Analysis Server library yet. +Future updates will allow for full class-access of Analysis Server protocol +objects. + +Analysis Server process must be instantiated separately and loaded into +Analysis Server Client. To learn how to generate an Analysis Server Process, +refer to the [Analysis Server page.](https://github.com/dart-lang/sdk/tree/master/pkg/analysis_server) diff --git a/pkg/analysis_server_client/lib/analysis_server_client.dart b/pkg/analysis_server_client/lib/analysis_server_client.dart new file mode 100644 index 00000000000..96286e40dd9 --- /dev/null +++ b/pkg/analysis_server_client/lib/analysis_server_client.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +/// Type of callbacks used to process notification. +typedef void NotificationProcessor(String event, Map params); + +/// Instances of the class [AnalysisServerClient] manage a connection to an +/// [AnalysisServer] process, and facilitate communication to and from the +/// client/user. +class AnalysisServerClient { + /// AnalysisServer process object, or null if the server has been shut down. + final Process _process; + + /// Commands that have been sent to the server but not yet acknowledged, + /// and the [Completer] objects which should be completed when + /// acknowledgement is received. + final Map _pendingCommands = {}; + + /// Number which should be used to compute the 'id' to send to the next + /// command sent to the server. + int _nextId = 0; + + AnalysisServerClient(this._process); + + /// Return a future that will complete when all commands that have been + /// sent to the server so far have been flushed to the OS buffer. + Future flushCommands() { + return _process.stdin.flush(); + } + + void listenToOutput({NotificationProcessor notificationProcessor}) { + _process.stdout + .transform((new Utf8Codec()).decoder) + .transform(new LineSplitter()) + .listen((String line) { + String trimmedLine = line.trim(); + if (trimmedLine.startsWith('Observatory listening on ')) { + return; + } + final result = JSON.decoder.convert(trimmedLine) as Map; + if (result.containsKey('id')) { + final id = result['id'] as String; + final completer = _pendingCommands.remove(id); + + if (result.containsKey('error')) { + completer.completeError(new ServerErrorMessage(result['error'])); + } else { + completer.complete(result['result']); + } + } else if (notificationProcessor != null && result.containsKey('event')) { + // Message is a notification. It should have an event and possibly + // params. + notificationProcessor(result['event'], result['params']); + } + }); + } + + /// Sends a command to the server. An 'id' will be automatically assigned. + /// The returned [Future] will be completed when the server acknowledges + /// the command with a response. If the server acknowledges the command + /// with a normal (non-error) response, the future will be completed + /// with the 'result' field from the response. If the server acknowledges + /// the command with an error response, the future will be completed with an + /// error. + Future send(String method, Map params) { + String id = '${_nextId++}'; + Map command = { + 'id': id, + 'method': method + }; + if (params != null) { + command['params'] = params; + } + Completer completer = new Completer(); + _pendingCommands[id] = completer; + String commandAsJson = JSON.encode(command); + _process.stdin.add(UTF8.encoder.convert('$commandAsJson\n')); + return completer.future; + } + + /// Force kill the server. Returns exit code future. + Future kill() { + _process.kill(); + return _process.exitCode; + } +} + +class ServerErrorMessage { + final Map errorJson; + + ServerErrorMessage(this.errorJson); + + String get code => errorJson['code'].toString(); + String get message => errorJson['message']; + String get stackTrace => errorJson['stackTrace']; +} diff --git a/pkg/analysis_server_client/pubspec.yaml b/pkg/analysis_server_client/pubspec.yaml new file mode 100644 index 00000000000..25ea57e87a0 --- /dev/null +++ b/pkg/analysis_server_client/pubspec.yaml @@ -0,0 +1,13 @@ +name: analysis_server_client +version: 1.0.1 +author: Dart Team +description: + A client wrapper over analysis_server. Instances of this client + manage a connection to the analysis_server process and + facilitates communication to and from the server. +homepage: http://github.com/dart-lang/sdk/pkg/analysis_server_client +dev_dependencies: + test: ">=0.12.0 <0.13.0" + mockito: ^2.0.2 +environment: + sdk: ">=1.0.0 < 2.0.0-dev.infinity" diff --git a/pkg/analysis_server_client/test/analysis_server_client_test.dart b/pkg/analysis_server_client/test/analysis_server_client_test.dart new file mode 100644 index 00000000000..4e2f5acfa31 --- /dev/null +++ b/pkg/analysis_server_client/test/analysis_server_client_test.dart @@ -0,0 +1,93 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:analysis_server_client/analysis_server_client.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +void main() { + Process _process; + AnalysisServerClient serverWrapper; + + setUp(() async { + _process = new MockProcess(); + serverWrapper = new AnalysisServerClient(_process); + when(_process.stdin).thenReturn([]); + }); + + test('test_listenToOutput_good', () async { + when(_process.stdout).thenReturn(_goodMessage()); + + final future = serverWrapper.send('blahMethod', null); + serverWrapper.listenToOutput(); + + final response = await future; + expect(response, new isInstanceOf()); + final responseAsMap = response as Map; + expect(responseAsMap['foo'], 'bar'); + }); + + test('test_listenToOutput_error', () async { + when(_process.stdout).thenReturn(_badMessage()); + final future = serverWrapper.send('blahMethod', null); + future.catchError((e) { + expect(e, new isInstanceOf()); + final e2 = e as ServerErrorMessage; + expect(e2.code, 'someErrorCode'); + expect(e2.message, 'something went wrong'); + expect(e2.stackTrace, 'some long stack trace'); + }); + serverWrapper.listenToOutput(); + }); + + test('test_listenToOutput_event', () async { + when(_process.stdout).thenReturn(_eventMessage()); + + void eventHandler(String event, Map params) { + expect(event, 'fooEvent'); + expect(params.length, 2); + expect(params['foo'] as String, 'bar'); + expect(params['baz'] as String, 'bang'); + } + + serverWrapper.send('blahMethod', null); + serverWrapper.listenToOutput(notificationProcessor: eventHandler); + }); +} + +Stream> _goodMessage() async* { + yield UTF8.encoder.convert('Observatory listening on foo bar\n'); + final sampleJson = { + 'id': '0', + 'result': {'foo': 'bar'} + }; + yield UTF8.encoder.convert(JSON.encode(sampleJson)); +} + +final _badErrorMessage = { + 'code': 'someErrorCode', + 'message': 'something went wrong', + 'stackTrace': 'some long stack trace' +}; + +Stream> _badMessage() async* { + yield UTF8.encoder.convert('Observatory listening on foo bar\n'); + final sampleJson = {'id': '0', 'error': _badErrorMessage}; + yield UTF8.encoder.convert(JSON.encode(sampleJson)); +} + +Stream> _eventMessage() async* { + yield UTF8.encoder.convert('Observatory listening on foo bar\n'); + final sampleJson = { + 'event': 'fooEvent', + 'params': {'foo': 'bar', 'baz': 'bang'} + }; + yield UTF8.encoder.convert(JSON.encode(sampleJson)); +} + +class MockProcess extends Mock implements Process {}