Meld server runner in dart fix with analysis_server_client
Change-Id: I86a2e7b815ff6d3beea2d968c5e34c4e3feae2c3 Reviewed-on: https://dart-review.googlesource.com/c/82080 Commit-Queue: Dan Rubel <danrubel@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
committed by
commit-bot@chromium.org
parent
98a0309fbd
commit
645ec2e8b5
@@ -1,101 +0,0 @@
|
||||
// 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<String, Object> 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<String, Completer> _pendingCommands = <String, Completer>{};
|
||||
|
||||
/// 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<Null> flushCommands() {
|
||||
return _process.stdin.flush();
|
||||
}
|
||||
|
||||
/// Force kill the server. Returns exit code future.
|
||||
Future<int> kill() {
|
||||
_process.kill();
|
||||
return _process.exitCode;
|
||||
}
|
||||
|
||||
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<String, dynamic> params) {
|
||||
String id = '${_nextId++}';
|
||||
Map<String, dynamic> command = <String, dynamic>{
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
class ServerErrorMessage {
|
||||
final Map errorJson;
|
||||
|
||||
ServerErrorMessage(this.errorJson);
|
||||
|
||||
String get code => errorJson['code'].toString();
|
||||
String get message => errorJson['message'];
|
||||
String get stackTrace => errorJson['stackTrace'];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2018, 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 'package:analysis_server_client/server.dart';
|
||||
|
||||
/// A subclass of [Server] that caches all messages exchanged with the server.
|
||||
/// This is primarily used when testing and debugging the analysis server.
|
||||
/// Most clients will want to use [Server] rather than this class.
|
||||
class RecordingServer extends Server {
|
||||
/// True if we are currently printing out messages exchanged with the server.
|
||||
bool _echoMessages = false;
|
||||
|
||||
/// Messages which have been exchanged with the server; we buffer these
|
||||
/// up until the test finishes, so that they can be examined in the debugger
|
||||
/// or printed out in response to a call to [echoMessages].
|
||||
final _messages = <String>[];
|
||||
|
||||
/// Print out any messages exchanged with the server. If some messages have
|
||||
/// already been exchanged with the server, they are printed out immediately.
|
||||
void echoMessages() {
|
||||
if (_echoMessages) {
|
||||
return;
|
||||
}
|
||||
_echoMessages = true;
|
||||
for (String line in _messages) {
|
||||
print(line);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> kill([String reason = 'none']) {
|
||||
echoMessages();
|
||||
return super.kill(reason);
|
||||
}
|
||||
|
||||
@override
|
||||
void logBadDataFromServer(String details, {bool silent: false}) {
|
||||
echoMessages();
|
||||
super.logBadDataFromServer(details, silent: silent);
|
||||
}
|
||||
|
||||
/// Record a message that was exchanged with the server,
|
||||
/// and print it out if [echoMessages] has been called.
|
||||
@override
|
||||
void logMessage(String prefix, String details) {
|
||||
String line = '$currentElapseTime: $prefix $details';
|
||||
if (_echoMessages) {
|
||||
print(line);
|
||||
}
|
||||
_messages.add(line);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) 2018, 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:path/path.dart';
|
||||
|
||||
/// Type of callbacks used to process notifications.
|
||||
typedef void NotificationProcessor(String event, Map<String, dynamic> params);
|
||||
|
||||
/// Instances of the class [Server] manage a connection to a server process,
|
||||
/// and facilitate communication to and from the server.
|
||||
class Server {
|
||||
/// Server process object, or `null` if server hasn't been started yet.
|
||||
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 _pendingCommands = <String, Completer<Map<String, dynamic>>>{};
|
||||
|
||||
/// Number which should be used to compute the 'id'
|
||||
/// to send in the next command sent to the server.
|
||||
int _nextId = 0;
|
||||
|
||||
/// True if we've received bad data from the server.
|
||||
bool _receivedBadDataFromServer = false;
|
||||
|
||||
/// Stopwatch that we use to generate timing information for debug output.
|
||||
Stopwatch _time = new Stopwatch();
|
||||
|
||||
/// The [currentElapseTime] at which the last communication was received from
|
||||
/// the server or `null` if no communication has been received.
|
||||
double lastCommunicationTime;
|
||||
|
||||
Server([Process process]) : this._process = process;
|
||||
|
||||
/// The current elapse time (seconds) since the server was started.
|
||||
double get currentElapseTime => _time.elapsedTicks / _time.frequency;
|
||||
|
||||
/// Future that completes when the server process exits.
|
||||
Future<int> get exitCode => _process.exitCode;
|
||||
|
||||
/// 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<void> flushCommands() => _process.stdin.flush();
|
||||
|
||||
/// Force kill the server. Returns exit code future.
|
||||
Future<int> kill([String reason = 'none']) {
|
||||
logMessage('FORCIBLY TERMINATING PROCESS: ', reason);
|
||||
_process.kill();
|
||||
return _process.exitCode;
|
||||
}
|
||||
|
||||
/// Start listening to output from the server,
|
||||
/// and deliver notifications to [notificationProcessor].
|
||||
void listenToOutput({NotificationProcessor notificationProcessor}) {
|
||||
_process.stdout
|
||||
.transform(utf8.decoder)
|
||||
.transform(new LineSplitter())
|
||||
.listen((String line) {
|
||||
lastCommunicationTime = currentElapseTime;
|
||||
String trimmedLine = line.trim();
|
||||
|
||||
// Guard against lines like:
|
||||
// {"event":"server.connected","params":{...}}Observatory listening on ...
|
||||
const observatoryMessage = 'Observatory listening on ';
|
||||
if (trimmedLine.contains(observatoryMessage)) {
|
||||
trimmedLine = trimmedLine
|
||||
.substring(0, trimmedLine.indexOf(observatoryMessage))
|
||||
.trim();
|
||||
}
|
||||
if (trimmedLine.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
logMessage('<== ', trimmedLine);
|
||||
Map<String, dynamic> message;
|
||||
try {
|
||||
message = json.decoder.convert(trimmedLine);
|
||||
} catch (exception) {
|
||||
logBadDataFromServer('JSON decode failure: $exception');
|
||||
return;
|
||||
}
|
||||
|
||||
final id = message['id'];
|
||||
if (id != null) {
|
||||
// Handle response
|
||||
final completer = _pendingCommands.remove(id);
|
||||
if (completer == null) {
|
||||
throw 'Unexpected response from server: id=$id';
|
||||
}
|
||||
if (message.containsKey('error')) {
|
||||
completer.completeError(new ServerErrorMessage(message));
|
||||
} else {
|
||||
completer.complete(message['result']);
|
||||
}
|
||||
} else {
|
||||
// Handle notification
|
||||
final String event = message['event'];
|
||||
if (event != null) {
|
||||
if (notificationProcessor != null) {
|
||||
notificationProcessor(event, message['params']);
|
||||
}
|
||||
} else {
|
||||
logBadDataFromServer('Unexpected message from server');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_process.stderr
|
||||
.transform(utf8.decoder)
|
||||
.transform(new LineSplitter())
|
||||
.listen((String line) {
|
||||
String trimmedLine = line.trim();
|
||||
logMessage('ERR: ', trimmedLine);
|
||||
logBadDataFromServer('Message received on stderr', silent: true);
|
||||
});
|
||||
}
|
||||
|
||||
/// Send 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<Map<String, dynamic>> send(
|
||||
String method, Map<String, dynamic> params) {
|
||||
String id = '${_nextId++}';
|
||||
Map<String, dynamic> command = <String, dynamic>{
|
||||
'id': id,
|
||||
'method': method
|
||||
};
|
||||
if (params != null) {
|
||||
command['params'] = params;
|
||||
}
|
||||
final completer = new Completer<Map<String, dynamic>>();
|
||||
_pendingCommands[id] = completer;
|
||||
String line = json.encode(command);
|
||||
logMessage('==> ', line);
|
||||
_process.stdin.add(utf8.encoder.convert("$line\n"));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server.
|
||||
*
|
||||
* If [profileServer] is `true`, the server will be started
|
||||
* with "--observe" and "--pause-isolates-on-exit", allowing the observatory
|
||||
* to be used.
|
||||
*
|
||||
* If [serverPath] is specified, then that analysis server will be launched,
|
||||
* otherwise the analysis server snapshot in the SDK will be launched.
|
||||
*/
|
||||
Future start({
|
||||
int diagnosticPort,
|
||||
String instrumentationLogFile,
|
||||
bool profileServer: false,
|
||||
String sdkPath,
|
||||
String serverPath,
|
||||
int servicesPort,
|
||||
bool suppressAnalytics: true,
|
||||
bool useAnalysisHighlight2: false,
|
||||
}) async {
|
||||
if (_process != null) {
|
||||
throw new Exception('Process already started');
|
||||
}
|
||||
_time.start();
|
||||
String dartBinary = Platform.executable;
|
||||
|
||||
// The integration tests run 3x faster when run from snapshots
|
||||
// (you need to run test.py with --use-sdk).
|
||||
if (serverPath == null) {
|
||||
// Look for snapshots/analysis_server.dart.snapshot.
|
||||
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
|
||||
'snapshots', 'analysis_server.dart.snapshot'));
|
||||
|
||||
if (!FileSystemEntity.isFileSync(serverPath)) {
|
||||
// Look for dart-sdk/bin/snapshots/analysis_server.dart.snapshot.
|
||||
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
|
||||
'dart-sdk', 'bin', 'snapshots', 'analysis_server.dart.snapshot'));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> 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}');
|
||||
}
|
||||
//
|
||||
// Add the server executable.
|
||||
//
|
||||
arguments.add(serverPath);
|
||||
//
|
||||
// Add server arguments.
|
||||
//
|
||||
if (suppressAnalytics) {
|
||||
arguments.add('--suppress-analytics');
|
||||
}
|
||||
if (diagnosticPort != null) {
|
||||
arguments.add('--port');
|
||||
arguments.add(diagnosticPort.toString());
|
||||
}
|
||||
if (instrumentationLogFile != null) {
|
||||
arguments.add('--instrumentation-log-file=$instrumentationLogFile');
|
||||
}
|
||||
if (sdkPath != null) {
|
||||
arguments.add('--sdk=$sdkPath');
|
||||
}
|
||||
if (useAnalysisHighlight2) {
|
||||
arguments.add('--useAnalysisHighlight2');
|
||||
}
|
||||
logMessage(
|
||||
'Starting analysis server: ', '$dartBinary ${arguments.join(' ')}');
|
||||
_process = await Process.start(dartBinary, arguments);
|
||||
_process.exitCode.then((int code) {
|
||||
if (code != 0) {
|
||||
logBadDataFromServer('server terminated with exit code $code');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Deal with bad data received from the server.
|
||||
void logBadDataFromServer(String details, {bool silent: false}) {
|
||||
if (!silent) {
|
||||
logMessage('BAD DATA FROM SERVER: ', details);
|
||||
}
|
||||
if (_receivedBadDataFromServer) {
|
||||
// We're already dealing with it.
|
||||
return;
|
||||
}
|
||||
_receivedBadDataFromServer = true;
|
||||
// Give the server 1 second to continue outputting bad data
|
||||
// such as outputting a stacktrace.
|
||||
new Future.delayed(new Duration(seconds: 1), () {
|
||||
throw 'Bad data received from server: $details';
|
||||
});
|
||||
}
|
||||
|
||||
/// Log a message that was exchanged with the server.
|
||||
/// Subclasses may override as needed.
|
||||
void logMessage(String prefix, String details) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
/// An error result from a server request.
|
||||
class ServerErrorMessage {
|
||||
final Map<String, dynamic> message;
|
||||
|
||||
ServerErrorMessage(this.message);
|
||||
|
||||
Map<String, dynamic> get error => message['error'];
|
||||
get errorCode => error['code'];
|
||||
get errorMessage => error['message'];
|
||||
get stackTrace => error['stackTrace'];
|
||||
|
||||
String toString() => message.toString();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2018, 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 'server_test.dart' as server_test;
|
||||
|
||||
main() {
|
||||
server_test.main();
|
||||
}
|
||||
+21
-13
@@ -6,23 +6,24 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:analysis_server_client/analysis_server_client.dart';
|
||||
import 'package:analysis_server_client/server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
MockProcess process;
|
||||
AnalysisServerClient serverWrapper;
|
||||
Server server;
|
||||
|
||||
setUp(() async {
|
||||
process = new MockProcess();
|
||||
serverWrapper = new AnalysisServerClient(process);
|
||||
server = new Server(process);
|
||||
});
|
||||
|
||||
test('test_listenToOutput_good', () async {
|
||||
process.stdout = _goodMessage();
|
||||
process.stderr = _noMessage();
|
||||
|
||||
final future = serverWrapper.send('blahMethod', null);
|
||||
serverWrapper.listenToOutput();
|
||||
final future = server.send('blahMethod', null);
|
||||
server.listenToOutput();
|
||||
|
||||
final response = await future;
|
||||
expect(response, const TypeMatcher<Map>());
|
||||
@@ -32,19 +33,22 @@ void main() {
|
||||
|
||||
test('test_listenToOutput_error', () async {
|
||||
process.stdout = _badMessage();
|
||||
final future = serverWrapper.send('blahMethod', null);
|
||||
process.stderr = _noMessage();
|
||||
|
||||
final future = server.send('blahMethod', null);
|
||||
future.catchError((e) {
|
||||
expect(e, const TypeMatcher<ServerErrorMessage>());
|
||||
final e2 = e as ServerErrorMessage;
|
||||
expect(e2.code, 'someErrorCode');
|
||||
expect(e2.message, 'something went wrong');
|
||||
expect(e2.stackTrace, 'some long stack trace');
|
||||
final error = e as ServerErrorMessage;
|
||||
expect(error.errorCode, 'someErrorCode');
|
||||
expect(error.errorMessage, 'something went wrong');
|
||||
expect(error.stackTrace, 'some long stack trace');
|
||||
});
|
||||
serverWrapper.listenToOutput();
|
||||
server.listenToOutput();
|
||||
});
|
||||
|
||||
test('test_listenToOutput_event', () async {
|
||||
process.stdout = _eventMessage();
|
||||
process.stderr = _noMessage();
|
||||
|
||||
void eventHandler(String event, Map<String, Object> params) {
|
||||
expect(event, 'fooEvent');
|
||||
@@ -53,8 +57,8 @@ void main() {
|
||||
expect(params['baz'] as String, 'bang');
|
||||
}
|
||||
|
||||
serverWrapper.send('blahMethod', null);
|
||||
serverWrapper.listenToOutput(notificationProcessor: eventHandler);
|
||||
server.send('blahMethod', null);
|
||||
server.listenToOutput(notificationProcessor: eventHandler);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,6 +92,10 @@ Stream<List<int>> _goodMessage() async* {
|
||||
yield utf8.encoder.convert(json.encode(sampleJson));
|
||||
}
|
||||
|
||||
Stream<List<int>> _noMessage() async* {
|
||||
yield utf8.encoder.convert('');
|
||||
}
|
||||
|
||||
class MockProcess implements Process {
|
||||
@override
|
||||
Stream<List<int>> stderr;
|
||||
@@ -6,10 +6,11 @@ import 'dart:async';
|
||||
import 'dart:io' show File, Platform;
|
||||
|
||||
import 'package:analysis_server_client/protocol.dart';
|
||||
import 'package:analysis_server_client/server.dart';
|
||||
import 'package:cli_util/cli_logging.dart';
|
||||
import 'package:dartfix/src/context.dart';
|
||||
import 'package:dartfix/src/verbose_server.dart';
|
||||
import 'package:dartfix/src/options.dart';
|
||||
import 'package:dartfix/src/server.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class Driver {
|
||||
@@ -25,11 +26,24 @@ class Driver {
|
||||
|
||||
Ansi get ansi => logger.ansi;
|
||||
|
||||
bool get runAnalysisServerFromSource {
|
||||
// Automatically run analysis server from source
|
||||
// if this command line tool is being run from source
|
||||
// within the source tree.
|
||||
return Server.findRoot() != null;
|
||||
/// Return the analysis_server executable by proceeding upward
|
||||
/// until finding the Dart SDK repository root then returning
|
||||
/// the analysis_server executable within the repository.
|
||||
/// Return `null` if it cannot be found.
|
||||
String findServerPath() {
|
||||
String pathname = Platform.script.toFilePath();
|
||||
while (true) {
|
||||
String parent = path.dirname(pathname);
|
||||
if (parent.length >= pathname.length) {
|
||||
return null;
|
||||
}
|
||||
String serverPath =
|
||||
path.join(parent, 'pkg', 'analysis_server', 'bin', 'server.dart');
|
||||
if (new File(serverPath).existsSync()) {
|
||||
return serverPath;
|
||||
}
|
||||
pathname = parent;
|
||||
}
|
||||
}
|
||||
|
||||
Future start(List<String> args) async {
|
||||
@@ -66,20 +80,22 @@ class Driver {
|
||||
}
|
||||
|
||||
Future startServer(Options options) async {
|
||||
server = new Server(logger);
|
||||
server = logger.isVerbose ? new VerboseServer(logger) : new Server();
|
||||
const connectTimeout = const Duration(seconds: 15);
|
||||
serverConnected = new Completer();
|
||||
if (options.verbose) {
|
||||
server.debugStdio();
|
||||
logger.trace('Dart SDK version ${Platform.version}');
|
||||
logger.trace(' ${Platform.resolvedExecutable}');
|
||||
logger.trace('dartfix');
|
||||
logger.trace(' ${Platform.script.toFilePath()}');
|
||||
}
|
||||
final runFromSource = runAnalysisServerFromSource;
|
||||
logger.trace(runFromSource ? 'Starting from source...' : 'Starting...');
|
||||
await server.start(sdkPath: options.sdkPath, useSnapshot: !runFromSource);
|
||||
server.listenToOutput(dispatchNotification);
|
||||
// Automatically run analysis server from source
|
||||
// if this command line tool is being run from source within the SDK repo.
|
||||
String serverPath = findServerPath();
|
||||
logger
|
||||
.trace(serverPath != null ? 'Starting from source...' : 'Starting...');
|
||||
await server.start(sdkPath: options.sdkPath, serverPath: serverPath);
|
||||
server.listenToOutput(notificationProcessor: handleEvent);
|
||||
await serverConnected.future.timeout(connectTimeout, onTimeout: () {
|
||||
logger.stderr('Failed to connect to server');
|
||||
context.exit(15);
|
||||
@@ -192,7 +208,7 @@ class Driver {
|
||||
|
||||
/// Dispatch the notification named [event], and containing parameters
|
||||
/// [params], to the appropriate stream.
|
||||
void dispatchNotification(String event, params) {
|
||||
void handleEvent(String event, params) {
|
||||
ResponseDecoder decoder = new ResponseDecoder(null);
|
||||
switch (event) {
|
||||
case SERVER_NOTIFICATION_CONNECTED:
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
// Copyright (c) 2018, 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:cli_util/cli_logging.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
/**
|
||||
* Type of callbacks used to process notifications.
|
||||
*/
|
||||
typedef void NotificationProcessor(String event, params);
|
||||
|
||||
/**
|
||||
* Instances of the class [Server] manage a connection to a server process, and
|
||||
* facilitate communication to and from the server.
|
||||
*/
|
||||
class Server {
|
||||
/**
|
||||
* Server process object, or null if server hasn't been started yet.
|
||||
*/
|
||||
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<String, Completer<Map<String, dynamic>>> _pendingCommands =
|
||||
<String, Completer<Map<String, dynamic>>>{};
|
||||
|
||||
/**
|
||||
* Number which should be used to compute the 'id' to send in the next command
|
||||
* sent to the server.
|
||||
*/
|
||||
int _nextId = 0;
|
||||
|
||||
/**
|
||||
* Messages which have been exchanged with the server; we buffer these
|
||||
* up until the test finishes, so that they can be examined in the debugger
|
||||
* or printed out in response to a call to [debugStdio].
|
||||
*/
|
||||
final List<String> _recordedStdio = <String>[];
|
||||
|
||||
/**
|
||||
* True if we are currently printing out messages exchanged with the server.
|
||||
*/
|
||||
bool _debuggingStdio = false;
|
||||
|
||||
/**
|
||||
* True if we've received bad data from the server, and we are aborting the
|
||||
* test.
|
||||
*/
|
||||
bool _receivedBadDataFromServer = false;
|
||||
|
||||
/**
|
||||
* Stopwatch that we use to generate timing information for debug output.
|
||||
*/
|
||||
Stopwatch _time = new Stopwatch();
|
||||
|
||||
/**
|
||||
* The [currentElapseTime] at which the last communication was received from the server
|
||||
* or `null` if no communication has been received.
|
||||
*/
|
||||
double lastCommunicationTime;
|
||||
|
||||
/**
|
||||
* The current elapse time (seconds) since the server was started.
|
||||
*/
|
||||
double get currentElapseTime => _time.elapsedTicks / _time.frequency;
|
||||
|
||||
/**
|
||||
* Future that completes when the server process exits.
|
||||
*/
|
||||
Future<int> get exitCode => _process.exitCode;
|
||||
|
||||
final Logger logger;
|
||||
|
||||
Server(this.logger);
|
||||
|
||||
/**
|
||||
* Print out any messages exchanged with the server. If some messages have
|
||||
* already been exchanged with the server, they are printed out immediately.
|
||||
*/
|
||||
void debugStdio() {
|
||||
if (_debuggingStdio) {
|
||||
return;
|
||||
}
|
||||
_debuggingStdio = true;
|
||||
for (String line in _recordedStdio) {
|
||||
logger.trace(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the root directory of the analysis_server package by proceeding
|
||||
* upward until finding the Dart SDK repository root then returning
|
||||
* the analysis_server package root within the repository.
|
||||
* Return `null` if it cannot be found.
|
||||
*/
|
||||
static String findRoot([String pathname]) {
|
||||
pathname ??= Platform.script.toFilePath(windows: Platform.isWindows);
|
||||
while (true) {
|
||||
String parent = dirname(pathname);
|
||||
if (parent.length >= pathname.length) {
|
||||
return null;
|
||||
}
|
||||
String root = normalize(join(parent, 'pkg', 'analysis_server'));
|
||||
String server = join(root, 'bin', 'server.dart');
|
||||
if (new File(server).existsSync()) {
|
||||
return root;
|
||||
}
|
||||
pathname = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server.
|
||||
*/
|
||||
Future<int> kill(String reason) {
|
||||
debugStdio();
|
||||
_recordStdio('FORCIBLY TERMINATING PROCESS: $reason');
|
||||
_process.kill();
|
||||
return _process.exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening to output from the server, and deliver notifications to
|
||||
* [notificationProcessor].
|
||||
*/
|
||||
void listenToOutput(NotificationProcessor notificationProcessor) {
|
||||
_process.stdout
|
||||
.transform(utf8.decoder)
|
||||
.transform(new LineSplitter())
|
||||
.listen((String line) {
|
||||
lastCommunicationTime = currentElapseTime;
|
||||
String trimmedLine = line.trim();
|
||||
|
||||
// Guard against lines like:
|
||||
// {"event":"server.connected","params":{...}}Observatory listening on ...
|
||||
final String observatoryMessage = 'Observatory listening on ';
|
||||
if (trimmedLine.contains(observatoryMessage)) {
|
||||
trimmedLine = trimmedLine
|
||||
.substring(0, trimmedLine.indexOf(observatoryMessage))
|
||||
.trim();
|
||||
}
|
||||
if (trimmedLine.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_recordStdio('<== $trimmedLine');
|
||||
var message;
|
||||
try {
|
||||
message = json.decoder.convert(trimmedLine);
|
||||
} catch (exception) {
|
||||
_badDataFromServer('JSON decode failure: $exception');
|
||||
return;
|
||||
}
|
||||
Map messageAsMap = message;
|
||||
if (messageAsMap.containsKey('id')) {
|
||||
String id = message['id'];
|
||||
Completer<Map<String, dynamic>> completer = _pendingCommands[id];
|
||||
if (completer == null) {
|
||||
throw 'Unexpected response from server: id=$id';
|
||||
} else {
|
||||
_pendingCommands.remove(id);
|
||||
}
|
||||
if (messageAsMap.containsKey('error')) {
|
||||
completer.completeError(new ServerErrorMessage(messageAsMap));
|
||||
} else {
|
||||
Map<String, dynamic> result = messageAsMap['result'];
|
||||
completer.complete(result);
|
||||
}
|
||||
} else {
|
||||
String event = messageAsMap['event'];
|
||||
notificationProcessor(event, messageAsMap['params']);
|
||||
}
|
||||
});
|
||||
_process.stderr
|
||||
.transform((new Utf8Codec()).decoder)
|
||||
.transform(new LineSplitter())
|
||||
.listen((String line) {
|
||||
String trimmedLine = line.trim();
|
||||
_recordStdio('ERR: $trimmedLine');
|
||||
_badDataFromServer('Message received on stderr', silent: true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send 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<Map<String, dynamic>> send(
|
||||
String method, Map<String, dynamic> params) {
|
||||
String id = '${_nextId++}';
|
||||
Map<String, dynamic> command = <String, dynamic>{
|
||||
'id': id,
|
||||
'method': method
|
||||
};
|
||||
if (params != null) {
|
||||
command['params'] = params;
|
||||
}
|
||||
Completer<Map<String, dynamic>> completer =
|
||||
new Completer<Map<String, dynamic>>();
|
||||
_pendingCommands[id] = completer;
|
||||
String line = json.encode(command);
|
||||
_recordStdio('==> $line');
|
||||
_process.stdin.add(utf8.encoder.convert("$line\n"));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server. If [profileServer] is `true`, the server will be started
|
||||
* with "--observe" and "--pause-isolates-on-exit", allowing the observatory
|
||||
* to be used.
|
||||
*/
|
||||
Future start({
|
||||
int diagnosticPort,
|
||||
String instrumentationLogFile,
|
||||
bool profileServer: false,
|
||||
String sdkPath,
|
||||
int servicesPort,
|
||||
bool useAnalysisHighlight2: false,
|
||||
bool useSnapshot: true,
|
||||
}) async {
|
||||
if (_process != null) {
|
||||
throw new Exception('Process already started');
|
||||
}
|
||||
_time.start();
|
||||
String dartBinary = Platform.executable;
|
||||
|
||||
String serverPath;
|
||||
|
||||
// The integration tests run 3x faster when run from snapshots (you need to
|
||||
// run test.py with --use-sdk).
|
||||
if (useSnapshot) {
|
||||
// Look for snapshots/analysis_server.dart.snapshot.
|
||||
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
|
||||
'snapshots', 'analysis_server.dart.snapshot'));
|
||||
|
||||
if (!FileSystemEntity.isFileSync(serverPath)) {
|
||||
// Look for dart-sdk/bin/snapshots/analysis_server.dart.snapshot.
|
||||
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
|
||||
'dart-sdk', 'bin', 'snapshots', 'analysis_server.dart.snapshot'));
|
||||
}
|
||||
} else {
|
||||
String rootDir = Server.findRoot();
|
||||
if (rootDir == null) {
|
||||
throw new Exception("Can't find analysis server root directory");
|
||||
}
|
||||
serverPath = normalize(join(rootDir, 'bin', 'server.dart'));
|
||||
}
|
||||
|
||||
List<String> 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}');
|
||||
}
|
||||
//
|
||||
// Add the server executable.
|
||||
//
|
||||
arguments.add(serverPath);
|
||||
//
|
||||
// Add server arguments.
|
||||
//
|
||||
arguments.add('--suppress-analytics');
|
||||
if (diagnosticPort != null) {
|
||||
arguments.add('--port');
|
||||
arguments.add(diagnosticPort.toString());
|
||||
}
|
||||
if (instrumentationLogFile != null) {
|
||||
arguments.add('--instrumentation-log-file=$instrumentationLogFile');
|
||||
}
|
||||
if (sdkPath != null) {
|
||||
arguments.add('--sdk=$sdkPath');
|
||||
}
|
||||
if (useAnalysisHighlight2) {
|
||||
arguments.add('--useAnalysisHighlight2');
|
||||
}
|
||||
_process = await Process.start(dartBinary, arguments);
|
||||
_process.exitCode.then((int code) {
|
||||
if (code != 0) {
|
||||
_badDataFromServer('server terminated with exit code $code');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deal with bad data received from the server.
|
||||
*/
|
||||
void _badDataFromServer(String details, {bool silent: false}) {
|
||||
if (!silent) {
|
||||
_recordStdio('BAD DATA FROM SERVER: $details');
|
||||
}
|
||||
if (_receivedBadDataFromServer) {
|
||||
// We're already dealing with it.
|
||||
return;
|
||||
}
|
||||
_receivedBadDataFromServer = true;
|
||||
debugStdio();
|
||||
// Give the server 1 second to continue outputting bad data
|
||||
// such as outputting a stacktrace.
|
||||
new Future.delayed(new Duration(seconds: 1), () {
|
||||
throw 'Bad data received from server: $details';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a message that was exchanged with the server, and print it out if
|
||||
* [debugStdio] has been called.
|
||||
*/
|
||||
void _recordStdio(String line) {
|
||||
double elapsedTime = currentElapseTime;
|
||||
line = "$elapsedTime: $line";
|
||||
if (_debuggingStdio) {
|
||||
logger.trace(line);
|
||||
}
|
||||
_recordedStdio.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An error result from a server request.
|
||||
*/
|
||||
class ServerErrorMessage {
|
||||
final Map message;
|
||||
|
||||
ServerErrorMessage(this.message);
|
||||
|
||||
dynamic get error => message['error'];
|
||||
|
||||
String toString() => message.toString();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2018, 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 'package:analysis_server_client/server.dart';
|
||||
import 'package:cli_util/cli_logging.dart';
|
||||
|
||||
class VerboseServer extends Server {
|
||||
final Logger logger;
|
||||
|
||||
VerboseServer(this.logger);
|
||||
|
||||
@override
|
||||
void logMessage(String prefix, String details) {
|
||||
logger.trace('$currentElapseTime: $prefix $details');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user