blob: 32b0c04d30f3154ba88ddc8c294521d85df9612d [file] [log] [blame]
// 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:analysis_server_client/listener/server_listener.dart';
import 'package:analysis_server_client/protocol.dart';
import 'package:path/path.dart';
/// Type of callbacks used to process notifications.
typedef void NotificationProcessor(Notification notification);
/// Instances of the class [Server] manage a server process,
/// and facilitate communication to and from the server.
///
/// Clients may not extend, implement or mix-in this class.
class Server {
/// If not `null`, [_listener] will be sent information
/// about interactions with the server.
ServerListener _listener;
/// Server process object, or `null` if server hasn't been started yet
/// or if the server has already been stopped.
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;
/// The stderr subscription or `null` if either
/// [listenToOutput] has not been called or [stop] has been called.
StreamSubscription<String> _stderrSubscription;
/// The stdout subscription or `null` if either
/// [listenToOutput] has not been called or [stop] has been called.
StreamSubscription<String> _stdoutSubscription;
Server({ServerListener listener, Process process})
: this._listener = listener,
this._process = process;
/// Force kill the server. Returns exit code future.
Future<int> kill({String reason = 'none'}) {
_listener?.killingServerProcess(reason);
final process = _process;
_process = null;
process.kill();
return process.exitCode;
}
/// Start listening to output from the server,
/// and deliver notifications to [notificationProcessor].
void listenToOutput({NotificationProcessor notificationProcessor}) {
_stdoutSubscription = _process.stdout
.transform(utf8.decoder)
.transform(new LineSplitter())
.listen((String line) {
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;
}
_listener?.messageReceived(trimmedLine);
Map<String, dynamic> message;
try {
message = json.decoder.convert(trimmedLine);
} catch (exception) {
_listener?.badMessage(trimmedLine, exception);
return;
}
final id = message[Response.ID];
if (id != null) {
// Handle response
final completer = _pendingCommands.remove(id);
if (completer == null) {
_listener?.unexpectedResponse(message, id);
}
if (message.containsKey(Response.ERROR)) {
completer.completeError(new RequestError.fromJson(
new ResponseDecoder(null), '.error', message[Response.ERROR]));
} else {
completer.complete(message[Response.RESULT]);
}
} else {
// Handle notification
final String event = message[Notification.EVENT];
if (event != null) {
if (notificationProcessor != null) {
notificationProcessor(
new Notification(event, message[Notification.PARAMS]));
}
} else {
_listener?.unexpectedMessage(message);
}
}
});
_stderrSubscription = _process.stderr
.transform(utf8.decoder)
.transform(new LineSplitter())
.listen((String line) {
String trimmedLine = line.trim();
_listener?.errorMessage(trimmedLine);
});
}
/// 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>{
Request.ID: id,
Request.METHOD: method
};
if (params != null) {
command[Request.PARAMS] = params;
}
final completer = new Completer<Map<String, dynamic>>();
_pendingCommands[id] = completer;
String line = json.encode(command);
_listener?.requestSent(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({
String clientId,
String clientVersion,
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');
}
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.
//
// TODO(danrubel): Consider moving all cmdline argument consts
// out of analysis_server and into analysis_server_client
if (clientId != null) {
arguments.add('--client-id');
arguments.add(clientId);
}
if (clientVersion != null) {
arguments.add('--client-version');
arguments.add(clientVersion);
}
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');
}
_listener?.startingServer(dartBinary, arguments);
_process = await Process.start(dartBinary, arguments);
_process.exitCode.then((int code) {
if (code != 0 && _process != null) {
// Report an error if server abruptly terminated
_listener?.unexpectedStop(code);
}
});
}
/// Attempt to gracefully shutdown the server.
/// If that fails, then kill the process.
Future<int> stop({Duration timeLimit}) async {
timeLimit ??= const Duration(seconds: 5);
if (_process == null) {
// Process already exited
return -1;
}
final future = send(SERVER_REQUEST_SHUTDOWN, null);
final process = _process;
_process = null;
await future
// fall through to wait for exit
.timeout(timeLimit, onTimeout: () {
return null;
}).whenComplete(() async {
await _stderrSubscription?.cancel();
_stderrSubscription = null;
await _stdoutSubscription?.cancel();
_stdoutSubscription = null;
});
return process.exitCode.timeout(
timeLimit,
onTimeout: () {
_listener?.killingServerProcess('server failed to exit');
process.kill();
return process.exitCode;
},
);
}
}